Koskamo

Spur MCP

The Spur MCP server enables AI assistants to search for property owners, enrich contact data, generate AI-written outreach copy, and manage direct mail campaigns with trackable QR codes, all within a single workflow for real estate and home services lead generation.

Endpoint and access

No sign-in to connect and list tools. Individual tools may still need a vendor account, credentials, or permissions; check the documented prerequisites.

Spur MCP logo, by SpurVendor evidenceStreamable HTTPNo sign-inEndpoint checked 2026-11-01Operated by Spur
You need:Sign in with your Spur accountusage comes out of the same Spur plan allowance and wallet
MCP endpoint (Streamable HTTP)
https://mcp.usespur.com/mcp
Vendor documentation

What you can do with it

  • Find absentee-owned homes in a target area with specific equity filters
  • Draft personalized postcards or letters using AI copywriting
  • Mail printed postcards to property owners via USPS with QR tracking
  • Enrich owner data with names, mailing records, phones, and emails

Source and maintenance

Vendor evidenceRegistry 1.0.0

Protocol and capabilities

Server
spur-mcp-server 1.0.0
MCP protocol
2025-06-18
Tools
87 advertised
Resources
9 advertised
Resource templates
0 advertised
Prompts
7 advertised
Server-advertised capabilities
{
  "tools": {
    "listChanged": true
  },
  "prompts": {
    "listChanged": true
  },
  "resources": {
    "listChanged": true
  }
}

Server-provided instructions

Untrusted server output shown for inspection. It is not Koskamo guidance and is excluded from knowledge search.

Spur is a real-estate prospecting platform: find properties, skip-trace owners, and reach them by SMS, email and direct mail. Typical flow: spur_search_properties → spur_save_properties → spur_enrich (owner contacts) → outreach (spur_draft_message, spur_send_sms, workflows) or direct mail. Direct mail design: postcards come in 6x4, 6x9 and 6x11; letters are US Letter in one of two types, top_first_page (address shows through the envelope window, so the top 3.15in stays empty) or insert_blank_page (Spur adds an address page, so the letter keeps the full page). Start from blank (start_from_blank=true) or redesign an existing template (base_template_id, including Spur gallery designs) with spur_create_postcard_template / spur_create_letter_template, refine with the matching update tool, then mail: spur_preview_postcard_campaign → spur_send_postcards, or spur_preview_letter_campaign → spur_send_letters. QR codes on postcards: by default, design every postcard back with an empty QR code box (<div class="qr-box">) in the message area, unless the user says they don't want a QR code. Then fill it with spur_add_postcard_qr_code (never hand-write QR markup). Manage tracked codes with spur_list_qr_codes / spur_create_qr_code / spur_update_qr_code / spur_delete_qr_code; if none fits, ask the user where scans should go. See who scanned with spur_get_qr_code_activity. The user's own images (logos, headshots, property photos): spur_list_images gives their saved images with public URLs to use in postcard, letter and email HTML. To add one from their device, including an image they attached to the chat (attachments don't reach Spur), call spur_upload_image and let them drop it in; for a public link, spur_save_image. Prefer the user's saved images over stock photos when they fit. Email and SMS: design templates with spur_create_email_template (start_from_blank=true gives a responsive, compliant layout) or spur_create_sms_template, refine with the matching update tool, and check them with spur_preview_message_template (inbox/phone view, sample or real owner data) or spur_preview_message_draft before saving; spur_send_test_email sends one to the user's own inbox. Saved templates drive outreach workflows: spur_create_workflow builds a sequence (send_email / send_sms / send_direct_mail / wait steps) as an inactive draft, spur_set_workflow_step_template swaps a step's template, then spur_set_workflow_active and spur_enroll_owners_in_workflow start it after approval. Texting from the user's own number: spur_get_texting_setup shows their numbers, carrier (A2P 10DLC) registration status and the next step. Buy a local number with spur_search_phone_numbers → spur_buy_phone_number (after the user picks one). Bought numbers can only send once the registration is approved; until then texts go out from Spur's workspace number. Prices come from the wallet: plans include some numbers, extras are $5/mo; registration is $29 one-time then $3/mo ($79 + $15/mo for high volume); $15 per resubmission. Always state the price before buying or submitting. If fees go unpaid, texting pauses after 7 days: have the user top up, then spur_pay_texting_fees. Texts are billed per 160-character segment. Register with spur_save_texting_registration (write_campaign_text=true drafts the carrier text from their website), review it with the user, then spur_submit_texting_registration. Carriers only approve texting people who agreed to hear from the user: record only the opt-in methods they really use, and never present cold texting of purchased or skip-traced lists as compliant. Property research: translate the user's criteria into structured filters (spur_parse_search_query previews this for free), and use event filters for change or motivation signals (price drops, back on market, stale, distress stages, FSBO, equity gaps). If a search comes back thin, read the filter/event notes it returns and relax one constraint at a time instead of guessing. Recurring research belongs in spur_create_search_automation with the same filters. Paying for Spur: when the user wants to add funds, buy credits, upgrade or compare plans, or a tool fails with "Billing limit reached", call spur_show_billing_options. It renders one-click Pay and Upgrade buttons. Or go straight to spur_top_up_wallet (amount_usd) or spur_change_plan (plan_id, billing_interval) when the user named what they want. Both return a Stripe checkout link: share it, and once the user says they paid, call spur_check_payment with its session_id. When a send is short on funds, suggest a top-up that covers the shortfall (round up to $25, $50 or $100) and offer spur_set_auto_recharge so it doesn't happen again. Rules: - Tools that spend money or contact people need explicit user approval for that exact action (confirm=true). Never assume it. - Postcards and letters always go through their preview tool first (spur_preview_postcard_campaign / spur_preview_letter_campaign); send only with that preview's confirmation_token after the user approves. - Check spur_get_billing_summary before large sends or enrichment batches. - Never ask for card numbers in the chat; payment happens only on Stripe's page. Switching an existing paid subscription and turning on auto-recharge charge the saved card, so they need confirm=true after explicit approval. - Refer to properties and owners by address/name, not ids, when talking to the user. - Spur tools are the only source for the user's templates, postcards, properties and owners. Never supplement them with web search, image search or outside examples (e.g. Etsy, Canva, Pinterest), and never invent details a tool didn't return. - When a tool renders a preview widget, that widget is the preview. Don't add other images or mockups, and don't describe the design beyond what the tool result says.

Tools

87 advertised

spur_search_properties — Search properties
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Search live listings (Zillow data) in Spur with plain English and/or structured filters, including event filters. Results show as cards and a map. Uses 1 property-search credit per call from the user's Spur plan, or 2 credits when the search makes more than 3 ZHomes calls (e.g. several event types, tax/sale-history events, or description-based filters). To check how a sentence will be interpreted before spending a credit, call spur_parse_search_query first. Examples: - "3 bed houses under $2,500/mo in Austin owned 10+ years" -> location="Austin, TX", listing_status="forRent", home_type="singleFamily", min_beds=3, max_price=2500, min_ownership_years=10 - "duplexes for sale in 78704 built before 1980 with absentee owners" -> location="78704", listing_status="forSale", home_type="multiFamily", max_year_built=1979, only_absentee=true - "new pre-foreclosures or price drops of 10%+ in Dallas this week" -> location="Dallas, TX", events=["preforeclosure","price_drop"], event_within_days=7, event_min_change_pct=10 - "motivated sellers in Houston: stale, back on market, or fired their agent" -> location="Houston, TX", events=["stale_listing","back_on_market","agent_to_fsbo"] Events (pass in `events`; a listing matches when ANY selected event fired, and is tagged with each): - Listing lifecycle: new_listing, coming_soon, coming_soon_active, price_drop, price_increase, pending, sold, back_on_market, delisted (automations only), open_house, rental_listed, rent_change, move_in_moved_up - Distress & motivation: preforeclosure, foreclosure, auction, bank_owned, distress_escalated, fsbo, agent_to_fsbo, stale_listing, new_construction - Valuation & tax: tax_assessment_posted, assessed_value_jump, tax_paid_dropped (weak), sale_recorded, zestimate_moved, equity_gap, price_below_assessed Direct events (price_drop, pending, sold, preforeclosure, fsbo, new_listing, …) work on the first search. Change events (price_increase, back_on_market, distress_escalated, agent_to_fsbo, rent_change, zestimate_moved, …) compare against what Spur saw on an earlier search of the same area, so the first search only records a baseline. Motivated-seller research usually combines price_drop, back_on_market, stale_listing, agent_to_fsbo and the distress stages. Structured filters always win over what Spur infers from `query`; use query for qualitative intent (condition, style, "brick", "needs work") that only listing descriptions can match. Unset filters are left to the query. Follow up with spur_save_properties to save listings the user likes (pass the cards' listing objects), or spur_create_search_automation to run the same filters on a schedule.

spur_search_properties input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "limit": {
      "type": "integer",
      "default": 20,
      "maximum": 50,
      "minimum": 1,
      "description": "Maximum results to show"
    },
    "query": {
      "type": "string",
      "maxLength": 1500,
      "description": "Free-text intent; Spur's AI turns it into filters and re-ranks by description"
    },
    "views": {
      "type": "array",
      "items": {
        "enum": [
          "water",
          "city",
          "mountain",
          "park",
          "golfCourse"
        ],
        "type": "string"
      },
      "minItems": 1,
      "description": "Only listings with any of these view types"
    },
    "events": {
      "type": "array",
      "items": {
        "enum": [
          "new_listing",
          "coming_soon",
          "coming_soon_active",
          "price_drop",
          "price_increase",
          "pending",
          "sold",
          "back_on_market",
          "delisted",
          "open_house",
          "rental_listed",
          "rent_change",
          "move_in_moved_up",
          "preforeclosure",
          "foreclosure",
          "auction",
          "bank_owned",
          "distress_escalated",
          "fsbo",
          "agent_to_fsbo",
          "stale_listing",
          "new_construction",
          "tax_assessment_posted",
          "assessed_value_jump",
          "tax_paid_dropped",
          "sale_recorded",
          "zestimate_moved",
          "equity_gap",
          "price_below_assessed"
        ],
        "type": "string"
      },
      "maxItems": 15,
      "minItems": 1,
      "description": "Only listings where any of these events happened; see the tool description"
    },
    "max_age": {
      "type": "integer",
      "maximum": 400,
      "minimum": 0,
      "description": "At most this many years since built"
    },
    "max_zip": {
      "type": "integer",
      "maximum": 99999,
      "minimum": 0,
      "description": "Highest ZIP code in a numeric ZIP range (with min_zip)"
    },
    "min_age": {
      "type": "integer",
      "maximum": 400,
      "minimum": 0,
      "description": "Years since built"
    },
    "min_zip": {
      "type": "integer",
      "maximum": 99999,
      "minimum": 0,
      "description": "Lowest ZIP code in a numeric ZIP range (with max_zip); prefer zip_list for specific ZIPs"
    },
    "has_pool": {
      "type": "boolean",
      "description": "true = only listings with a pool"
    },
    "has_view": {
      "type": "boolean",
      "description": "true = only listings with any view (use views on live search for a specific kind)"
    },
    "keywords": {
      "type": "string",
      "maxLength": 300,
      "description": "Comma-separated listing keywords, e.g. \"pool, granite\""
    },
    "location": {
      "type": "string",
      "maxLength": 200,
      "description": "City and state (\"Dallas, TX\") or ZIP code"
    },
    "max_beds": {
      "type": "integer",
      "maximum": 20,
      "minimum": 0,
      "description": "Maximum bedrooms"
    },
    "max_sqft": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Maximum living area in square feet"
    },
    "min_beds": {
      "type": "integer",
      "maximum": 20,
      "minimum": 0,
      "description": "Minimum bedrooms"
    },
    "min_sqft": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Minimum living area in square feet"
    },
    "zip_list": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{5}$"
      },
      "maxItems": 50,
      "minItems": 1,
      "description": "Keep only these ZIPs; set location to the city that contains them"
    },
    "home_type": {
      "enum": [
        "singleFamily",
        "condo,apartment",
        "townhome",
        "multiFamily",
        "manufactured",
        "land"
      ],
      "type": "string",
      "description": "Zillow home type code"
    },
    "max_baths": {
      "type": "number",
      "maximum": 20,
      "minimum": 0,
      "description": "Maximum bathrooms"
    },
    "max_price": {
      "type": "number",
      "minimum": 0,
      "description": "Maximum price (monthly rent for rentals), USD"
    },
    "min_baths": {
      "type": "number",
      "maximum": 20,
      "minimum": 0,
      "description": "Minimum bathrooms (half baths count as 0.5)"
    },
    "min_price": {
      "type": "number",
      "minimum": 0,
      "description": "Minimum price (monthly rent for rentals), USD"
    },
    "center_lat": {
      "type": "number",
      "maximum": 90,
      "minimum": -90,
      "description": "Radius search centre (with center_lng and radius_miles)"
    },
    "center_lng": {
      "type": "number",
      "maximum": 180,
      "minimum": -180,
      "description": "Radius search centre longitude (with center_lat and radius_miles)"
    },
    "has_garage": {
      "type": "boolean",
      "description": "true = only listings with a garage"
    },
    "min_equity": {
      "type": "number",
      "minimum": 0,
      "description": "Estimated equity in USD (Zestimate or price minus assessed value; rough)"
    },
    "days_posted": {
      "type": "integer",
      "maximum": 3650,
      "minimum": 1,
      "description": "Only listings posted within this many days"
    },
    "has_3d_tour": {
      "type": "boolean",
      "description": "true = only listings with a 3D virtual tour"
    },
    "only_vacant": {
      "type": "boolean",
      "description": "Likely vacant (heuristic: description keywords + long days on market)"
    },
    "listing_type": {
      "enum": [
        "fsba",
        "fsbo",
        "newConstruction",
        "foreclosure",
        "preforeclosure",
        "foreclosed",
        "auction"
      ],
      "type": "string",
      "description": "Seller / listing kind; for catching NEW entries into a distress stage prefer events"
    },
    "max_lot_size": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Square feet"
    },
    "min_lot_size": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Square feet (1 acre = 43,560)"
    },
    "radius_miles": {
      "type": "number",
      "maximum": 500,
      "minimum": 0,
      "description": "Keep only listings within this many miles of center_lat / center_lng"
    },
    "is_waterfront": {
      "type": "boolean",
      "description": "true = only waterfront listings"
    },
    "neighborhoods": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 80,
        "minLength": 1
      },
      "maxItems": 20,
      "minItems": 1,
      "description": "Neighborhood / area names matched against address and description"
    },
    "only_absentee": {
      "type": "boolean",
      "description": "Absentee-owned (owner mails elsewhere)"
    },
    "has_open_house": {
      "type": "boolean",
      "description": "true = only listings with an upcoming open house"
    },
    "listing_status": {
      "enum": [
        "forRent",
        "forSale",
        "sold"
      ],
      "type": "string",
      "description": "Defaults to forRent when omitted and the query doesn't imply one"
    },
    "max_year_built": {
      "type": "integer",
      "maximum": 2100,
      "minimum": 1700,
      "description": "Built in or before this year"
    },
    "min_year_built": {
      "type": "integer",
      "maximum": 2100,
      "minimum": 1700,
      "description": "Built in or after this year"
    },
    "ownership_type": {
      "enum": [
        "individual",
        "business",
        "any"
      ],
      "type": "string",
      "description": "business = LLC / corp / trust owners (heuristic)"
    },
    "is_single_story": {
      "type": "boolean",
      "description": "true = only single-story homes"
    },
    "event_stale_days": {
      "type": "integer",
      "maximum": 3650,
      "minimum": 1,
      "description": "Days on market that counts as stale (default 90)"
    },
    "is_price_reduced": {
      "type": "boolean",
      "description": "true = only listings whose asking price has been cut"
    },
    "event_within_days": {
      "type": "integer",
      "maximum": 365,
      "minimum": 1,
      "description": "Lookback window for events, in days (default 7)"
    },
    "max_days_on_market": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Listed at most this many days"
    },
    "min_days_on_market": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Listed at least this many days (higher suggests a motivated seller)"
    },
    "max_ownership_years": {
      "type": "number",
      "maximum": 150,
      "minimum": 0,
      "description": "Owner has held the property at most this long"
    },
    "min_ownership_years": {
      "type": "number",
      "maximum": 150,
      "minimum": 0,
      "description": "Owner has held the property at least this long"
    },
    "event_min_change_pct": {
      "type": "number",
      "maximum": 100,
      "minimum": 0,
      "description": "Minimum % move for price / rent / Zestimate / assessment events"
    },
    "has_air_conditioning": {
      "type": "boolean",
      "description": "true = only listings with air conditioning"
    },
    "only_pre_foreclosure": {
      "type": "boolean",
      "description": "Distressed / pre-foreclosure (heuristic, includes description keywords)"
    },
    "event_min_equity_gap_pct": {
      "type": "number",
      "maximum": 1000,
      "minimum": 0,
      "description": "Minimum % Zestimate above last sale price for equity_gap (default 30)"
    }
  },
  "additionalProperties": false
}
spur_search_properties annotations
{
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_parse_search_query — Preview search filters
Advertised by the server during the last successful anonymous check.
Advertised read-only

Show how Spur interprets a plain-English property search (filters and events) without running it. Free: uses no search credit. Use it to confirm intent before spending a credit, or to turn a vague request into explicit filters you can adjust. The returned arguments can be passed straight to spur_search_properties (keep the same location / status unless the user says otherwise). Exclusions ("no condos", "not foreclosures") are honoured by leaving those filters out.

spur_parse_search_query input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "query"
  ],
  "properties": {
    "query": {
      "type": "string",
      "maxLength": 1500,
      "minLength": 1,
      "description": "The user's search in their own words"
    }
  },
  "additionalProperties": false
}
spur_parse_search_query annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_property — Get property
Advertised by the server during the last successful anonymous check.
Advertised read-onlyOpen-world

Show one property as a detailed card: a saved Spur property by property_id, or a live listing by zpid. Includes owner links and enrichment status for saved properties.

spur_get_property input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "zpid": {
      "type": [
        "string",
        "number"
      ],
      "description": "Zillow property id from search results"
    },
    "property_id": {
      "type": "string",
      "description": "Saved Spur property id"
    }
  },
  "additionalProperties": false
}
spur_get_property annotations
{
  "readOnlyHint": true,
  "openWorldHint": true,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_save_properties — Save properties
Advertised by the server during the last successful anonymous check.

Save live listings into the user's Spur properties (and optionally a property list). Pass listing objects from spur_search_properties structuredContent.cards[].listing. Already-saved listings are refreshed, not duplicated.

spur_save_properties input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "listings"
  ],
  "properties": {
    "list_id": {
      "type": "string",
      "description": "Also add the saved properties to this property list (spur_list_property_lists)"
    },
    "listings": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "zpid",
          "address"
        ],
        "properties": {
          "zpid": {
            "type": [
              "number",
              "string"
            ]
          },
          "address": {
            "type": "object",
            "properties": {
              "city": {
                "type": "string"
              },
              "state": {
                "type": "string"
              },
              "zipcode": {
                "type": "string"
              },
              "streetAddress": {
                "type": "string"
              }
            }
          }
        },
        "description": "A listing object exactly as returned in spur_search_properties structuredContent.cards[].listing",
        "additionalProperties": {}
      },
      "maxItems": 50,
      "minItems": 1,
      "description": "Listing objects copied unchanged from spur_search_properties structuredContent.cards[].listing"
    }
  },
  "additionalProperties": false
}
spur_save_properties annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_list_saved_properties — List saved properties
Advertised by the server during the last successful anonymous check.
Advertised read-only

List properties the user has saved in Spur, optionally filtered by city, state, list, or enrichment status. Shows cards and a map.

spur_list_saved_properties input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "city": {
      "type": "string",
      "description": "City name, e.g. \"Austin\""
    },
    "limit": {
      "type": "integer",
      "default": 20,
      "maximum": 100,
      "minimum": 1,
      "description": "Maximum number of items to return"
    },
    "state": {
      "type": "string",
      "description": "Two-letter state, e.g. \"TX\""
    },
    "offset": {
      "type": "integer",
      "default": 0,
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Number of items to skip, for pagination"
    },
    "list_id": {
      "type": "string",
      "description": "Only properties in this property list"
    },
    "max_price": {
      "type": "number",
      "description": "Maximum price in USD (rent per month for rentals)"
    },
    "min_price": {
      "type": "number",
      "description": "Minimum price in USD (rent per month for rentals)"
    },
    "enrichment_status": {
      "enum": [
        "pending",
        "enriched",
        "failed",
        "partial"
      ],
      "type": "string",
      "description": "Skip-trace status of the property's owner (spur_enrich)"
    }
  },
  "additionalProperties": false
}
spur_list_saved_properties annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_property_stats — Property stats
Advertised by the server during the last successful anonymous check.
Advertised read-only

Totals for the user's saved properties: counts by status, enrichment progress and similar portfolio-level numbers.

spur_get_property_stats input schema
{
  "type": "object",
  "properties": {}
}
spur_get_property_stats annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_find_owners — Find owners
Advertised by the server during the last successful anonymous check.
Advertised read-only

Find property owners in the user's Spur account by name, phone, email or mailing address text. Use spur_get_owner for full details and portfolio.

spur_find_owners input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "limit": {
      "type": "integer",
      "default": 20,
      "maximum": 100,
      "minimum": 1,
      "description": "Maximum number of items to return"
    },
    "query": {
      "type": "string",
      "maxLength": 200,
      "description": "Text to match against name, email, phone or mailing address"
    },
    "offset": {
      "type": "integer",
      "default": 0,
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Number of items to skip, for pagination"
    },
    "has_phone": {
      "type": "boolean",
      "description": "Only owners with at least one phone number"
    },
    "enrichment_status": {
      "enum": [
        "enriched",
        "pending",
        "failed",
        "partial",
        "not_enriched"
      ],
      "type": "string",
      "description": "Only owners with this skip-trace status"
    }
  },
  "additionalProperties": false
}
spur_find_owners annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_owner — Get owner
Advertised by the server during the last successful anonymous check.
Advertised read-only

Full owner record: contact details, mailing address, enrichment status and their property portfolio in Spur.

spur_get_owner input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "owner_id"
  ],
  "properties": {
    "owner_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Owner id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_get_owner annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_enrich — Skip trace / enrich
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Run skip tracing to find owners and their phone numbers, emails and mailing addresses. - property_ids: find the owner of saved properties (creates/links owner records) - owner_ids: refresh contact details for existing owners Costs enrichment credits per record (check spur_get_billing_summary). Ask the user to approve the count first, then call with confirm=true.

spur_enrich input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "owner_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 100,
      "description": "Owner ids to skip-trace (from spur_find_owners)"
    },
    "property_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 100,
      "description": "Saved property ids whose owners to skip-trace"
    }
  },
  "additionalProperties": false
}
spur_enrich annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_list_property_lists — List property lists
Advertised by the server during the last successful anonymous check.
Advertised read-only

All of the user's property lists with ids and sizes. Use the id with spur_list_saved_properties, spur_add_properties_to_list or outreach tools.

spur_list_property_lists input schema
{
  "type": "object",
  "properties": {}
}
spur_list_property_lists annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_create_property_list — Create property list
Advertised by the server during the last successful anonymous check.

Create a property list, optionally seeding it with saved property ids.

spur_create_property_list input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "name"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "List name shown in Spur"
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Optional note about what the list is for"
    },
    "property_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 500,
      "description": "Saved property ids to add right away"
    }
  },
  "additionalProperties": false
}
spur_create_property_list annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_add_properties_to_list — Add properties to list
Advertised by the server during the last successful anonymous check.

Add saved properties to an existing property list.

spur_add_properties_to_list input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "list_id",
    "property_ids"
  ],
  "properties": {
    "list_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Property list id (UUID from a Spur list/search tool)"
    },
    "property_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 500,
      "minItems": 1,
      "description": "Saved property ids (from spur_list_saved_properties or spur_save_properties)"
    }
  },
  "additionalProperties": false
}
spur_add_properties_to_list annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_draft_message — Draft outreach message
Advertised by the server during the last successful anonymous check.

Draft an email, SMS or direct-mail message with Spur's AI. Returns text only; nothing is sent. Pass owner_id/property_id to personalize with real owner and property data. Uses 1 AI credit. Use {{merge_fields}} like {{owner_first_name}} and {{property_address}} to keep it reusable (see resource spur://merge-variables).

spur_draft_message input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "channel",
    "prompt"
  ],
  "properties": {
    "prompt": {
      "type": "string",
      "maxLength": 2000,
      "minLength": 3,
      "description": "What to write, e.g. \"friendly first touch asking if they would sell, mention we close in cash\""
    },
    "channel": {
      "enum": [
        "email",
        "sms",
        "direct_mail"
      ],
      "type": "string",
      "description": "Which kind of message to draft"
    },
    "owner_id": {
      "type": "string",
      "description": "Owner to personalize for (from spur_find_owners)"
    },
    "property_id": {
      "type": "string",
      "description": "Saved property the message is about"
    }
  },
  "additionalProperties": false
}
spur_draft_message annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_list_workflows — List outreach workflows
Advertised by the server during the last successful anonymous check.
Advertised read-only

The user's multi-step outreach workflows (email/SMS/mail sequences) with status and enrollment counts.

spur_list_workflows input schema
{
  "type": "object",
  "properties": {}
}
spur_list_workflows annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_workflow — Get outreach workflow
Advertised by the server during the last successful anonymous check.
Advertised read-only

One outreach workflow's steps in order (including reply/open branches built in the app), with the email, SMS or mail template each send step uses and its step_id.

spur_get_workflow input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "workflow_id"
  ],
  "properties": {
    "workflow_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Workflow id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_get_workflow annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_create_workflow — Create outreach workflow
Advertised by the server during the last successful anonymous check.

Create a multi-step outreach sequence from saved templates. It's saved as an inactive draft: nothing sends until it's activated (spur_set_workflow_active) and owners are enrolled. Send steps take a saved template: email/SMS from spur_list_message_templates (design new ones with spur_create_email_template / spur_create_sms_template), direct mail from the user's saved postcard or letter templates. Wait steps space them out. Reply/open/click branches can be added afterwards in Spur → Outreach. Example: "text, wait 2 days, email, wait 5 days, postcard" -> steps=[{type:"send_sms",template_id:…},{type:"wait",days:2},{type:"send_email",template_id:…},{type:"wait",days:5},{type:"send_direct_mail",template_id:…}]

spur_create_workflow input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "name",
    "steps"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "Workflow name shown in Spur"
    },
    "steps": {
      "type": "array",
      "items": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "type",
              "template_id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "const": "send_email"
              },
              "template_id": {
                "type": "string",
                "maxLength": 64,
                "minLength": 1,
                "description": "Email template id (UUID from a Spur list/search tool)"
              },
              "send_as_reply": {
                "type": "boolean",
                "description": "Reply in the previous email's thread"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "type",
              "template_id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "const": "send_sms"
              },
              "template_id": {
                "type": "string",
                "maxLength": 64,
                "minLength": 1,
                "description": "SMS template id (UUID from a Spur list/search tool)"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "type",
              "template_id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "const": "send_direct_mail"
              },
              "template_id": {
                "type": "string",
                "maxLength": 64,
                "minLength": 1,
                "description": "Saved postcard or letter template (spur_list_postcard_templates / spur_list_letter_templates; copy gallery designs first) id (UUID from a Spur list/search tool)"
              },
              "return_address_id": {
                "type": "string"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "days": {
                "type": "integer",
                "default": 0,
                "maximum": 365,
                "minimum": 0
              },
              "type": {
                "type": "string",
                "const": "wait"
              },
              "hours": {
                "type": "integer",
                "default": 0,
                "maximum": 23,
                "minimum": 0
              }
            },
            "additionalProperties": false
          }
        ]
      },
      "maxItems": 30,
      "minItems": 1,
      "description": "Steps in run order: send_email / send_sms / send_direct_mail with a saved template_id, and wait steps (days, hours) between them"
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Optional note about the workflow's goal"
    }
  },
  "additionalProperties": false
}
spur_create_workflow annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_set_workflow_step_template — Use template in workflow step
Advertised by the server during the last successful anonymous check.

Set which saved template a workflow's send step uses (send_email ← email template, send_sms ← SMS template, send_direct_mail ← postcard/letter template). Get step_ids from spur_get_workflow. If the workflow is active, enrolled owners get the new template from their next send.

spur_set_workflow_step_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "workflow_id",
    "step_id",
    "template_id"
  ],
  "properties": {
    "step_id": {
      "type": "string",
      "minLength": 1,
      "description": "step_id from spur_get_workflow"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Template id (UUID from a Spur list/search tool)"
    },
    "workflow_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Workflow id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_set_workflow_step_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_set_workflow_active — Activate or pause workflow
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Activate a workflow (enrolled owners start receiving its emails, texts and mail on schedule, which uses credits) or pause it. Activating requires every send step to have a template and the user's explicit approval.

spur_set_workflow_active input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "workflow_id",
    "active"
  ],
  "properties": {
    "active": {
      "type": "boolean",
      "description": "true = activate (start sending), false = pause"
    },
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Required (true) when activating, only after the user approved it"
    },
    "workflow_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Workflow id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_set_workflow_active annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_enroll_owners_in_workflow — Enroll owners in workflow
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Enroll owners in an outreach workflow. The workflow will start emailing/texting/mailing them on its schedule, which uses credits. Confirm the workflow name and owner count with the user first.

spur_enroll_owners_in_workflow input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "workflow_id",
    "owner_ids",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "owner_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 500,
      "minItems": 1,
      "description": "Owner ids to enroll (from spur_find_owners); owners without the needed contact details are skipped"
    },
    "workflow_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Workflow id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_enroll_owners_in_workflow annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_send_sms — Send SMS
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Text an owner from the user's default approved number (or Spur's workspace number if they have none; see spur_get_texting_setup). Show the user the exact message and recipient and get approval before calling with confirm=true. Billed per 160-character segment (70 if it has emoji or special characters), so a 200-character text costs 2.

spur_send_sms input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "owner_id",
    "message",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "message": {
      "type": "string",
      "maxLength": 1600,
      "minLength": 1,
      "description": "Exact SMS text the user approved"
    },
    "owner_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Owner id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_send_sms annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_list_sms_conversations — SMS inbox
Advertised by the server during the last successful anonymous check.
Advertised read-only

SMS conversations with owners, newest first, with unread counts and the last message. Good for triaging replies (hot leads, not interested, wrong number).

spur_list_sms_conversations input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "limit": {
      "type": "integer",
      "default": 25,
      "maximum": 100,
      "minimum": 1,
      "description": "Maximum conversations to return"
    },
    "unread_only": {
      "type": "boolean",
      "default": false,
      "description": "true = only conversations with unread replies"
    }
  },
  "additionalProperties": false
}
spur_list_sms_conversations annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_sms_conversation — SMS thread
Advertised by the server during the last successful anonymous check.
Advertised read-only

The full SMS thread with one owner, oldest first.

spur_get_sms_conversation input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "owner_id"
  ],
  "properties": {
    "owner_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Owner id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_get_sms_conversation annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_outreach_report — Outreach report
Advertised by the server during the last successful anonymous check.
Advertised read-only

Outreach performance (sent, delivered, opened, replied, by channel) for all workflows or one, over an optional date range.

spur_get_outreach_report input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "date_to": {
      "type": "string",
      "description": "ISO date"
    },
    "date_from": {
      "type": "string",
      "description": "ISO date, e.g. 2026-10-01"
    },
    "workflow_id": {
      "type": "string",
      "description": "Report on one workflow only; omit for all workflows"
    }
  },
  "additionalProperties": false
}
spur_get_outreach_report annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_list_message_templates — List email and SMS templates
Advertised by the server during the last successful anonymous check.
Advertised read-only

The user's saved email and SMS templates. These are what outreach workflow send_email / send_sms steps use.

spur_list_message_templates input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "channel": {
      "enum": [
        "email",
        "sms"
      ],
      "type": "string",
      "description": "Only email or only SMS templates; omit for both"
    }
  },
  "additionalProperties": false
}
spur_list_message_templates annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_message_template — Get email or SMS template
Advertised by the server during the last successful anonymous check.
Advertised read-only

Fetch an email template's subject, HTML and plain text, or an SMS template's text, for reading or editing. For small changes prefer the update tool's replacements over resending everything.

spur_get_message_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Email or SMS template id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_get_message_template annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_preview_message_template — Preview email or SMS template
Advertised by the server during the last successful anonymous check.
Advertised read-only

Show a saved email (inbox view, desktop and mobile) or SMS (phone view, segment count) rendered with sample data or a real owner's data, plus design checks. Changes nothing.

spur_preview_message_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "owner_id": {
      "type": "string",
      "description": "Preview with this owner's real data (spur_find_owners) instead of sample data"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Email or SMS template id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_preview_message_template annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_preview_message_draft — Preview unsaved email or SMS
Advertised by the server during the last successful anonymous check.
Advertised read-only

Preview email HTML or SMS text before saving it: renders it with sample or real owner data and runs the design checks. Nothing is saved; iterate here, then save with spur_create_email_template / spur_create_sms_template. Email HTML guidance (see spur://email-design-guide): full HTML document, table-based layout 600px wide, inline styles on elements (a small <style> block in <head> is fine for resets/media queries), web-safe font stacks, no JavaScript, no forms, no external CSS. Images as absolute https URLs with alt text and width attributes. The user's own images (logo, headshot, property photos): spur_list_images gives their URLs; if they want to use an image they have, call spur_upload_image (or spur_save_image for a link) and use the saved URL. Use PNG or JPG in emails, not SVG. A hidden preheader <div style="display:none"> right after <body> sets the inbox snippet. Include an opt-out line ("Reply STOP" or an unsubscribe link) and the sender's postal address in the footer. Keep it under 50,000 characters. Use merge fields like {{owner_first_name}}, {{property_address}}, {{your_name}}, {{your_phone}}, {{company_name}} (see spur://merge-variables). The plain-text version is generated from the HTML unless you pass text_body. SMS guidance: plain text, ideally one segment (160 GSM characters, or 70 if it uses emoji or other non-GSM characters; longer messages are billed per segment). Identify yourself, keep it conversational, and include opt-out wording ("Reply STOP to opt out") on first-touch messages. Links in texts from unverified numbers can be blocked, so give link-free fallback_body text when the body has a URL. Use merge fields like {{owner_first_name}}, {{property_address}}, {{your_name}} (see spur://merge-variables).

spur_preview_message_draft input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "channel"
  ],
  "properties": {
    "html": {
      "type": "string",
      "maxLength": 200000,
      "description": "Email HTML"
    },
    "text": {
      "type": "string",
      "maxLength": 10000,
      "description": "SMS text, or the email's plain-text version"
    },
    "channel": {
      "enum": [
        "email",
        "sms"
      ],
      "type": "string",
      "description": "Whether the draft is an email or an SMS"
    },
    "subject": {
      "type": "string",
      "maxLength": 500,
      "description": "Email subject"
    },
    "owner_id": {
      "type": "string",
      "description": "Preview with this owner's real data (spur_find_owners) instead of sample data"
    },
    "fallback_body": {
      "type": "string",
      "maxLength": 1600,
      "description": "SMS only: link-free text sent if links are blocked"
    }
  },
  "additionalProperties": false
}
spur_preview_message_draft annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_create_email_template — Create email template
Advertised by the server during the last successful anonymous check.

Save a new email template and show its inbox preview. Ways to start: - From blank: start_from_blank=true gives a tested 600px responsive layout with greeting, signature and a compliant footer; pass html to replace it. - Redesign: base_template_id copies one of the user's email templates. - From scratch: pass html (or only text_body for a plain-text email). The saved template can then be used by outreach workflows (spur_create_workflow / spur_set_workflow_step_template). Email HTML guidance (see spur://email-design-guide): full HTML document, table-based layout 600px wide, inline styles on elements (a small <style> block in <head> is fine for resets/media queries), web-safe font stacks, no JavaScript, no forms, no external CSS. Images as absolute https URLs with alt text and width attributes. The user's own images (logo, headshot, property photos): spur_list_images gives their URLs; if they want to use an image they have, call spur_upload_image (or spur_save_image for a link) and use the saved URL. Use PNG or JPG in emails, not SVG. A hidden preheader <div style="display:none"> right after <body> sets the inbox snippet. Include an opt-out line ("Reply STOP" or an unsubscribe link) and the sender's postal address in the footer. Keep it under 50,000 characters. Use merge fields like {{owner_first_name}}, {{property_address}}, {{your_name}}, {{your_phone}}, {{company_name}} (see spur://merge-variables). The plain-text version is generated from the HTML unless you pass text_body.

spur_create_email_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "name"
  ],
  "properties": {
    "html": {
      "type": "string",
      "maxLength": 50000,
      "description": "Full email HTML; merge fields like {{owner_first_name}} are filled per recipient"
    },
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "Template name shown in Spur"
    },
    "subject": {
      "type": "string",
      "maxLength": 500,
      "minLength": 1,
      "description": "Required unless copying a template"
    },
    "text_body": {
      "type": "string",
      "maxLength": 10000,
      "description": "Plain-text version; generated from the HTML when omitted"
    },
    "is_default": {
      "type": "boolean",
      "description": "Make this the user's default email template"
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Internal note about the template; never sent to owners"
    },
    "base_template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Copy this email template as the starting point"
    },
    "start_from_blank": {
      "type": "boolean",
      "description": "true = start from Spur's responsive, compliant blank email layout"
    }
  },
  "additionalProperties": false
}
spur_create_email_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_update_email_template — Edit email template
Advertised by the server during the last successful anonymous check.

Edit one of the user's email templates and show the updated inbox preview. For tweaks pass replacements (exact find → replace in "html", "subject" or "text"); to redesign pass full html. When the HTML changes, the plain-text version is regenerated unless it was hand-written or you pass text_body. Workflows using this template send the new version from their next step. Example: replacements=[{target:"html", find:"<p style=\"margin:0 0 16px;\"></p>", replace:"<p style=\"margin:0 0 16px;\">I'd like to make a cash offer on {{property_address}}.</p>"}] Email HTML guidance (see spur://email-design-guide): full HTML document, table-based layout 600px wide, inline styles on elements (a small <style> block in <head> is fine for resets/media queries), web-safe font stacks, no JavaScript, no forms, no external CSS. Images as absolute https URLs with alt text and width attributes. The user's own images (logo, headshot, property photos): spur_list_images gives their URLs; if they want to use an image they have, call spur_upload_image (or spur_save_image for a link) and use the saved URL. Use PNG or JPG in emails, not SVG. A hidden preheader <div style="display:none"> right after <body> sets the inbox snippet. Include an opt-out line ("Reply STOP" or an unsubscribe link) and the sender's postal address in the footer. Keep it under 50,000 characters. Use merge fields like {{owner_first_name}}, {{property_address}}, {{your_name}}, {{your_phone}}, {{company_name}} (see spur://merge-variables). The plain-text version is generated from the HTML unless you pass text_body.

spur_update_email_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "html": {
      "type": "string",
      "maxLength": 50000,
      "description": "Full replacement HTML"
    },
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "New template name"
    },
    "subject": {
      "type": "string",
      "maxLength": 500,
      "minLength": 1,
      "description": "New subject line"
    },
    "text_body": {
      "type": "string",
      "maxLength": 10000,
      "description": "Full replacement plain-text version"
    },
    "is_default": {
      "type": "boolean",
      "description": "true = make this the user's default template for its channel"
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Internal note about the template; never sent to owners"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Email or SMS template id (UUID from a Spur list/search tool)"
    },
    "replacements": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "target",
          "find",
          "replace"
        ],
        "properties": {
          "find": {
            "type": "string",
            "minLength": 1,
            "description": "Exact text that occurs once in that part"
          },
          "target": {
            "enum": [
              "html",
              "subject",
              "text"
            ],
            "type": "string",
            "description": "Which part to edit"
          },
          "replace": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "maxItems": 30,
      "description": "Small edits applied in order instead of resending the full content"
    }
  },
  "additionalProperties": false
}
spur_update_email_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_create_sms_template — Create SMS template
Advertised by the server during the last successful anonymous check.

Save a new SMS template and show its phone preview with segment count and checks. The saved template can then be used by outreach workflows (spur_create_workflow / spur_set_workflow_step_template). SMS guidance: plain text, ideally one segment (160 GSM characters, or 70 if it uses emoji or other non-GSM characters; longer messages are billed per segment). Identify yourself, keep it conversational, and include opt-out wording ("Reply STOP to opt out") on first-touch messages. Links in texts from unverified numbers can be blocked, so give link-free fallback_body text when the body has a URL. Use merge fields like {{owner_first_name}}, {{property_address}}, {{your_name}} (see spur://merge-variables).

spur_create_sms_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "name",
    "text"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "Template name shown in Spur"
    },
    "text": {
      "type": "string",
      "maxLength": 1600,
      "minLength": 1,
      "description": "SMS body; merge fields like {{owner_first_name}} are filled per recipient"
    },
    "is_default": {
      "type": "boolean",
      "description": "true = make this the user's default template for its channel"
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Internal note about the template; never sent to owners"
    },
    "fallback_body": {
      "type": "string",
      "maxLength": 1600,
      "description": "Link-free version sent if a link gets the text blocked"
    }
  },
  "additionalProperties": false
}
spur_create_sms_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_update_sms_template — Edit SMS template
Advertised by the server during the last successful anonymous check.

Edit one of the user's SMS templates and show the updated preview. Pass full text / fallback_body, or replacements (exact find → replace in "text" or "fallback"). Pass fallback_body="" to remove the fallback. SMS guidance: plain text, ideally one segment (160 GSM characters, or 70 if it uses emoji or other non-GSM characters; longer messages are billed per segment). Identify yourself, keep it conversational, and include opt-out wording ("Reply STOP to opt out") on first-touch messages. Links in texts from unverified numbers can be blocked, so give link-free fallback_body text when the body has a URL. Use merge fields like {{owner_first_name}}, {{property_address}}, {{your_name}} (see spur://merge-variables).

spur_update_sms_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "New template name"
    },
    "text": {
      "type": "string",
      "maxLength": 1600,
      "minLength": 1,
      "description": "Full replacement SMS body"
    },
    "is_default": {
      "type": "boolean",
      "description": "true = make this the user's default template for its channel"
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Internal note about the template; never sent to owners"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Email or SMS template id (UUID from a Spur list/search tool)"
    },
    "replacements": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "target",
          "find",
          "replace"
        ],
        "properties": {
          "find": {
            "type": "string",
            "minLength": 1,
            "description": "Exact text that occurs once in that part"
          },
          "target": {
            "enum": [
              "text",
              "fallback"
            ],
            "type": "string",
            "description": "Which part to edit"
          },
          "replace": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "maxItems": 30,
      "description": "Small edits applied in order instead of resending the full content"
    },
    "fallback_body": {
      "type": "string",
      "maxLength": 1600,
      "description": "Link-free text sent instead when links are blocked; \"\" removes it"
    }
  },
  "additionalProperties": false
}
spur_update_sms_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_delete_message_template — Delete email or SMS template
Advertised by the server during the last successful anonymous check.
Advertised destructive

Delete one of the user's email or SMS templates. Workflow steps that use it will fail until they get another template, so check spur_list_workflows and confirm with the user first.

spur_delete_message_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Email or SMS template id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_delete_message_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}
spur_send_test_email — Send test email to yourself
Advertised by the server during the last successful anonymous check.
Open-world

Send an email template, filled with sample data (or one owner's data), to the user's own Spur account email from their email sender, to check it in a real inbox. Never goes to owners. Confirm with the user first.

spur_send_test_email input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "owner_id": {
      "type": "string",
      "description": "Preview with this owner's real data (spur_find_owners) instead of sample data"
    },
    "sender_id": {
      "type": "string",
      "description": "Email sender to send from; defaults to the user's default sender"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Email or SMS template id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_send_test_email annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_list_postcard_templates — List postcard templates
Advertised by the server during the last successful anonymous check.
Advertised read-only

Postcard templates available to the user: their own (editable), Spur defaults, and Spur's built-in gallery. Copy a default or gallery design with spur_create_postcard_template base_template_id to redesign it, or start from blank with start_from_blank.

spur_list_postcard_templates input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "size": {
      "enum": [
        "6x4",
        "6x9",
        "6x11"
      ],
      "type": "string",
      "description": "Only templates designed for this size"
    },
    "category": {
      "type": "string",
      "description": "Only templates in this category, e.g. \"Just Sold\""
    },
    "include_gallery": {
      "type": "boolean",
      "default": true,
      "description": "Include Spur's built-in gallery designs"
    }
  },
  "additionalProperties": false
}
spur_list_postcard_templates annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_postcard_template — Get postcard template HTML
Advertised by the server during the last successful anonymous check.
Advertised read-only

Fetch a postcard template's HTML for reading or editing. For small text changes prefer spur_update_postcard_template with replacements instead of resending whole HTML.

spur_get_postcard_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "side": {
      "enum": [
        "front",
        "back",
        "both"
      ],
      "type": "string",
      "default": "both",
      "description": "Which side's HTML to return"
    },
    "template_id": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Template id from spur_list_postcard_templates (a saved template's UUID or a Spur gallery id)"
    }
  },
  "additionalProperties": false
}
spur_get_postcard_template annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_preview_postcard_template — Preview postcard template
Advertised by the server during the last successful anonymous check.
Advertised read-only

Show any postcard template (yours, a Spur default or a gallery design) rendered with sample data, without changing anything.

spur_preview_postcard_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "template_id": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Template id from spur_list_postcard_templates (a saved template's UUID or a Spur gallery id)"
    }
  },
  "additionalProperties": false
}
spur_preview_postcard_template annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_create_postcard_template — Create postcard template
Advertised by the server during the last successful anonymous check.

Create a postcard template and show a preview. Three ways to start: - From blank: start_from_blank=true gives an empty, correctly sized canvas for the size (any side you pass HTML for replaces the blank side). - Redesign an existing template: base_template_id (yours, a Spur default or a gallery id) copies its HTML; override front_html/back_html/size as needed. - From scratch: pass both front_html and back_html. HTML guidance (see spur://postcard-design-guide): full HTML documents, inline <style>, no scripts. Fixed pixel sizing on html, body and a root .postcard at 300 px/in including 0.125" bleed: 6x4 → 1875×1275, 6x9 → 2775×1875, 6x11 → 3375×1875. Keep text 0.25" inside the edge. USPS clear zone on the back, measured from the bleed edges (Spur prints over it and covers anything there): postage 1.25"×1.25" in the top-right corner; address block 3.5"×2", 0.5" from the right edge and 1.35" from the top; USPS barcode strip 4.75"×0.625" along the bottom-right edge. Keep all text, logos, photos and QR codes out of it; a background color or texture under it is fine. The safe message area is left of the address block and above the barcode strip. Images as absolute https URLs. The user's own images (logo, headshot, property photos): spur_list_images gives their URLs; if they want to use an image they have, call spur_upload_image (or spur_save_image for a link) and use the saved URL. Use merge fields such as {{owner_first_name}}, {{property_address}}, {{your_name}}, {{your_phone}}, {{company_name}} (see spur://merge-variables). QR code box by default: unless the user says they don't want a QR code, every back you design includes an empty square <div class="qr-box"> in the message area (≥ 0.75in, e.g. 225×225 px at 300 px/in) with a short call to action beside it ("Scan to see your home's value"). Never write QR markup or {{qrUrl}} yourself: after saving, call spur_add_postcard_qr_code and the tracked QR fills the box (pick the code with spur_list_qr_codes, or ask the user where scans should go and spur_create_qr_code). When resending full back_html, keep the qr-box and any <img data-spur-qr-merge…> inside it, or the QR is dropped.

spur_create_postcard_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "name"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "Template name shown in Spur"
    },
    "size": {
      "enum": [
        "6x4",
        "6x9",
        "6x11"
      ],
      "type": "string",
      "description": "Postcard size: \"6x4\" (standard 4×6), \"6x9\" or \"6x11\""
    },
    "category": {
      "type": "string",
      "maxLength": 100,
      "description": "Category label, e.g. \"Just Sold\" or \"Market Update\""
    },
    "back_html": {
      "type": "string",
      "maxLength": 500000,
      "description": "Full back-side HTML; keep the address block area clear"
    },
    "front_html": {
      "type": "string",
      "maxLength": 500000,
      "description": "Full front-side HTML"
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Internal note about the template; never printed"
    },
    "base_template_id": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Copy this template's HTML and size as the starting point"
    },
    "start_from_blank": {
      "type": "boolean",
      "description": "Start from an empty canvas sized for `size`"
    }
  },
  "additionalProperties": false
}
spur_create_postcard_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_update_postcard_template — Edit postcard template
Advertised by the server during the last successful anonymous check.

Edit one of the user's postcard templates and show the updated preview. For text tweaks pass replacements (exact find → replace on a side); to redesign, pass full front_html/back_html. Spur default and gallery templates can't be edited: copy them first with spur_create_postcard_template base_template_id. Example: change the headline -> replacements=[{side:"front", find:"Just Sold!", replace:"Just Listed!"}] HTML guidance (see spur://postcard-design-guide): full HTML documents, inline <style>, no scripts. Fixed pixel sizing on html, body and a root .postcard at 300 px/in including 0.125" bleed: 6x4 → 1875×1275, 6x9 → 2775×1875, 6x11 → 3375×1875. Keep text 0.25" inside the edge. USPS clear zone on the back, measured from the bleed edges (Spur prints over it and covers anything there): postage 1.25"×1.25" in the top-right corner; address block 3.5"×2", 0.5" from the right edge and 1.35" from the top; USPS barcode strip 4.75"×0.625" along the bottom-right edge. Keep all text, logos, photos and QR codes out of it; a background color or texture under it is fine. The safe message area is left of the address block and above the barcode strip. Images as absolute https URLs. The user's own images (logo, headshot, property photos): spur_list_images gives their URLs; if they want to use an image they have, call spur_upload_image (or spur_save_image for a link) and use the saved URL. Use merge fields such as {{owner_first_name}}, {{property_address}}, {{your_name}}, {{your_phone}}, {{company_name}} (see spur://merge-variables). QR code box by default: unless the user says they don't want a QR code, every back you design includes an empty square <div class="qr-box"> in the message area (≥ 0.75in, e.g. 225×225 px at 300 px/in) with a short call to action beside it ("Scan to see your home's value"). Never write QR markup or {{qrUrl}} yourself: after saving, call spur_add_postcard_qr_code and the tracked QR fills the box (pick the code with spur_list_qr_codes, or ask the user where scans should go and spur_create_qr_code). When resending full back_html, keep the qr-box and any <img data-spur-qr-merge…> inside it, or the QR is dropped.

spur_update_postcard_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "New template name"
    },
    "size": {
      "enum": [
        "6x4",
        "6x9",
        "6x11"
      ],
      "type": "string",
      "description": "Postcard size: \"6x4\" (standard 4×6), \"6x9\" or \"6x11\""
    },
    "category": {
      "type": "string",
      "maxLength": 100,
      "description": "Category label, e.g. \"Just Sold\" or \"Market Update\""
    },
    "back_html": {
      "type": "string",
      "maxLength": 500000,
      "description": "Full replacement back HTML"
    },
    "front_html": {
      "type": "string",
      "maxLength": 500000,
      "description": "Full replacement front HTML"
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Internal note about the template; never printed"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Template id (UUID from a Spur list/search tool)"
    },
    "replacements": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "side",
          "find",
          "replace"
        ],
        "properties": {
          "find": {
            "type": "string",
            "minLength": 1,
            "description": "Exact text that occurs once on that side"
          },
          "side": {
            "enum": [
              "front",
              "back"
            ],
            "type": "string",
            "description": "Which side to edit"
          },
          "replace": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "maxItems": 30,
      "description": "Small edits applied in order instead of resending the full content"
    }
  },
  "additionalProperties": false
}
spur_update_postcard_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_add_postcard_qr_code — Add QR code to postcard
Advertised by the server during the last successful anonymous check.

Put a tracked QR code on the back of one of the user's postcard templates and show the preview. Each mailed postcard gets its own code under the chosen QR code, so scans are tracked per recipient (spur_get_qr_code_activity). Placement: position="auto" fills the back's QR slot (an element with class qr-box, qr-code-box, qr-placeholder or qr-callout-box, as Spur's gallery designs have) and otherwise goes in the bottom-right of the message area, left of the address block and above the USPS barcode. Corners (top_left, top_right, bottom_left, bottom_right) and x_inches/y_inches place it in the message area; spots in the USPS clear zone are refused. Replaces any QR already on the card. If it lands on the message text, call again with another corner or move the text. Create a QR code first with spur_create_qr_code if spur_list_qr_codes has none that fits.

spur_add_postcard_qr_code input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id",
    "qr_code_id"
  ],
  "properties": {
    "position": {
      "enum": [
        "auto",
        "top_left",
        "top_right",
        "bottom_left",
        "bottom_right"
      ],
      "type": "string",
      "default": "auto",
      "description": "Where to place the code on the back; auto fills the design's qr-box"
    },
    "x_inches": {
      "type": "number",
      "maximum": 12,
      "minimum": 0,
      "description": "Left edge, inches from the bleed's left edge (with y_inches; overrides position)"
    },
    "y_inches": {
      "type": "number",
      "maximum": 7,
      "minimum": 0,
      "description": "Top edge, inches from the bleed's top edge (with x_inches)"
    },
    "qr_code_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "QR code id from spur_list_qr_codes or spur_create_qr_code"
    },
    "size_inches": {
      "type": "number",
      "default": 1,
      "maximum": 2,
      "minimum": 0.6,
      "description": "Edge length when placed in the message area (not used for a slot). 0.75in or more scans reliably"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Postcard template id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_add_postcard_qr_code annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_remove_postcard_qr_code — Remove QR code from postcard
Advertised by the server during the last successful anonymous check.

Remove the tracked QR code from one of the user's postcard templates (both sides) and show the preview. A QR slot from the design stays, empty.

spur_remove_postcard_qr_code input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Postcard template id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_remove_postcard_qr_code annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_delete_postcard_template — Delete postcard template
Advertised by the server during the last successful anonymous check.
Advertised destructive

Permanently delete one of the user's postcard templates. Spur defaults can't be deleted. Confirm with the user first.

spur_delete_postcard_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Template id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_delete_postcard_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}
spur_list_return_addresses — List return addresses
Advertised by the server during the last successful anonymous check.
Advertised read-only

Return addresses saved for direct mail. The default one is used when a campaign doesn't specify one.

spur_list_return_addresses input schema
{
  "type": "object",
  "properties": {}
}
spur_list_return_addresses annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_preview_postcard_campaign — Preview postcard mailing
Advertised by the server during the last successful anonymous check.
Advertised read-only

Required step before mailing postcards. Renders the postcard with the first recipient's real data, checks which owners have mailing addresses, estimates the cost from the user's plan, and returns a confirmation_token. Show the preview, then ask the user to approve. Only after they approve, call spur_send_postcards with the token (the widget's Send button does the same). The token expires in 30 minutes and only covers this exact template, recipient set, return address and size.

spur_preview_postcard_campaign input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id",
    "owner_ids"
  ],
  "properties": {
    "size": {
      "enum": [
        "6x4",
        "6x9",
        "6x11"
      ],
      "type": "string",
      "description": "Defaults to the template's size"
    },
    "owner_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 1000,
      "minItems": 1,
      "description": "Owners to mail (spur_find_owners)"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Postcard template id (UUID from a Spur list/search tool)"
    },
    "return_address_id": {
      "type": "string",
      "description": "Defaults to the user's default return address"
    }
  },
  "additionalProperties": false
}
spur_preview_postcard_campaign annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_send_postcards — Send postcards
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Mail the postcards approved in spur_preview_postcard_campaign. Irreversible and charges the user's account. Requires the preview's confirmation_token and the user's explicit approval (confirm=true).

spur_send_postcards input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "confirmation_token",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "confirmation_token": {
      "type": "string",
      "minLength": 20,
      "description": "confirmation_token from spur_preview_postcard_campaign"
    }
  },
  "additionalProperties": false
}
spur_send_postcards annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_list_qr_codes — List QR codes
Advertised by the server during the last successful anonymous check.
Advertised read-only

The user's tracked QR codes: destination URL, colors, and which of their postcard templates print each one. Put one on a postcard with spur_add_postcard_qr_code; see scans with spur_get_qr_code_activity.

spur_list_qr_codes input schema
{
  "type": "object",
  "properties": {}
}
spur_list_qr_codes annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_create_qr_code — Create QR code
Advertised by the server during the last successful anonymous check.

Create a tracked QR code that sends scans to destination_url. Then add it to a postcard back with spur_add_postcard_qr_code; each mailed postcard gets its own code under it so scans are tracked per recipient.

spur_create_qr_code input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "name",
    "destination_url"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "Label shown in Spur, e.g. 'Spring cash offer'"
    },
    "destination_url": {
      "type": "string",
      "maxLength": 2048,
      "minLength": 3,
      "description": "Where a scan sends people, e.g. https://acme.example/offer (https:// is added if missing)"
    },
    "background_color": {
      "type": "string",
      "pattern": "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
      "description": "Hex color, e.g. #111827. Keep strong contrast with the background so phones can scan it"
    },
    "foreground_color": {
      "type": "string",
      "pattern": "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
      "description": "Hex color, e.g. #111827. Keep strong contrast with the background so phones can scan it"
    }
  },
  "additionalProperties": false
}
spur_create_qr_code annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_update_qr_code — Edit QR code
Advertised by the server during the last successful anonymous check.

Rename a QR code, change where scans go, or change its colors. A new destination also applies to postcards already mailed with this code. Templates pick up new colors at send time.

spur_update_qr_code input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "qr_code_id"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "New name for the QR code"
    },
    "qr_code_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "QR code id from spur_list_qr_codes"
    },
    "destination_url": {
      "type": "string",
      "maxLength": 2048,
      "minLength": 3,
      "description": "Where a scan sends people, e.g. https://acme.example/offer (https:// is added if missing)"
    },
    "background_color": {
      "type": "string",
      "pattern": "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
      "description": "Hex color, e.g. #111827. Keep strong contrast with the background so phones can scan it"
    },
    "foreground_color": {
      "type": "string",
      "pattern": "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
      "description": "Hex color, e.g. #111827. Keep strong contrast with the background so phones can scan it"
    }
  },
  "additionalProperties": false
}
spur_update_qr_code annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_delete_qr_code — Delete QR code
Advertised by the server during the last successful anonymous check.
Advertised destructive

Permanently delete a QR code. Refused while one of the user's postcard templates still prints it: remove it from those first with spur_remove_postcard_qr_code. Postcards already mailed keep redirecting. Confirm with the user first.

spur_delete_qr_code input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "qr_code_id",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "qr_code_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "QR code id from spur_list_qr_codes"
    }
  },
  "additionalProperties": false
}
spur_delete_qr_code annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}
spur_get_qr_code_activity — QR code scans
Advertised by the server during the last successful anonymous check.
Advertised read-only

Scan activity for a QR code: totals, which mailed recipients (owners) scanned their postcard, and recent scans of the code itself. Use it to find warm leads from a postcard campaign.

spur_get_qr_code_activity input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "qr_code_id"
  ],
  "properties": {
    "limit": {
      "type": "integer",
      "default": 25,
      "maximum": 100,
      "minimum": 1,
      "description": "Maximum recipients and scans to list"
    },
    "qr_code_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "QR code id from spur_list_qr_codes"
    }
  },
  "additionalProperties": false
}
spur_get_qr_code_activity annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_list_images — List images
Advertised by the server during the last successful anonymous check.
Advertised read-only

The user's saved images (logos, headshots, property photos) with their public URLs, newest first. Use it as <img src="…"> (or a CSS background-image) in postcard, letter or email HTML. To add one, use spur_upload_image (from the user's device) or spur_save_image (from a URL).

spur_list_images input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "limit": {
      "type": "integer",
      "default": 30,
      "maximum": 100,
      "minimum": 1,
      "description": "Maximum images to return"
    }
  },
  "additionalProperties": false
}
spur_list_images annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_save_image — Save image from URL
Advertised by the server during the last successful anonymous check.
Open-world

Download an image from a public https URL (the user's website, a shared photo link) and save it to the user's Spur image library, so it stays available for printing and sending. Returns the saved image's URL. JPG, PNG, WebP, GIF or SVG, up to 20 MB. For a file on the user's device, use spur_upload_image instead. Only save images the user provided or asked for.

spur_save_image input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "url"
  ],
  "properties": {
    "url": {
      "type": "string",
      "format": "uri",
      "maxLength": 4096,
      "description": "Public https URL of the image"
    },
    "filename": {
      "type": "string",
      "maxLength": 120,
      "description": "Name to save it under, e.g. 'acme-logo'"
    }
  },
  "additionalProperties": false
}
spur_save_image annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_upload_image — Upload image
Advertised by the server during the last successful anonymous check.
Advertised read-only

Show an upload box where the user picks or drops an image from their device (logo, headshot, property photo); it's saved to their Spur image library. Use this whenever the user wants to use their own image, including one they attached to the chat: attachments don't reach Spur, so ask them to drop it into the box. After uploading, the user can click "Use this image", or call spur_list_images to get the URL.

spur_upload_image input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "purpose": {
      "type": "string",
      "maxLength": 120,
      "description": "What the image is for, shown to the user, e.g. 'Logo for the postcard back'"
    }
  },
  "additionalProperties": false
}
spur_upload_image annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_save_uploaded_image — Save uploaded image
Advertised by the server during the last successful anonymous check.

Used by the upload widget to save the file the user picked. Not for the assistant.

spur_save_uploaded_image input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "filename",
    "data_base64"
  ],
  "properties": {
    "filename": {
      "type": "string",
      "maxLength": 255,
      "description": "Original file name"
    },
    "data_base64": {
      "type": "string",
      "maxLength": 4194308,
      "minLength": 8,
      "description": "File contents, base64-encoded"
    }
  },
  "additionalProperties": false
}
spur_save_uploaded_image annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_delete_image — Delete image
Advertised by the server during the last successful anonymous check.
Advertised destructive

Permanently delete one of the user's saved images. Refused while one of their postcard, letter or email templates still uses it, since those would show a broken image. Confirm with the user first.

spur_delete_image input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "image_id",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "image_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "image_id from spur_list_images"
    }
  },
  "additionalProperties": false
}
spur_delete_image annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}
spur_list_letter_templates — List letter templates
Advertised by the server during the last successful anonymous check.
Advertised read-only

Letter templates available to the user, with their letter type (Top First Page or Insert Blank Page): their own (editable), Spur defaults, and Spur's built-in gallery. Copy one with spur_create_letter_template base_template_id to redesign it, or start from blank with start_from_blank.

spur_list_letter_templates input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "category": {
      "type": "string",
      "description": "Only templates in this category"
    },
    "letter_type": {
      "enum": [
        "top_first_page",
        "insert_blank_page"
      ],
      "type": "string",
      "description": "Only templates of this letter type"
    },
    "include_gallery": {
      "type": "boolean",
      "default": true,
      "description": "Include Spur's built-in gallery designs"
    }
  },
  "additionalProperties": false
}
spur_list_letter_templates annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_letter_template — Get letter template HTML
Advertised by the server during the last successful anonymous check.
Advertised read-only

Fetch a letter template's HTML for reading or editing. For small text changes prefer spur_update_letter_template with replacements instead of resending the whole HTML.

spur_get_letter_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "template_id": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Template id from spur_list_letter_templates (a saved template's UUID or a Spur gallery id)"
    }
  },
  "additionalProperties": false
}
spur_get_letter_template annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_preview_letter_template — Preview letter template
Advertised by the server during the last successful anonymous check.
Advertised read-only

Show any letter template (yours, a Spur default or a gallery design) rendered with sample data, without changing anything.

spur_preview_letter_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "template_id": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Template id from spur_list_letter_templates (a saved template's UUID or a Spur gallery id)"
    }
  },
  "additionalProperties": false
}
spur_preview_letter_template annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_create_letter_template — Create letter template
Advertised by the server during the last successful anonymous check.

Create a letter template and show a preview. Pick the letter type first (top_first_page or insert_blank_page); it decides where the address prints and how much of the page the design can use. Three ways to start: - From blank: start_from_blank=true gives an empty one-page letter laid out for the letter type. - Redesign an existing template: base_template_id (yours, a Spur default or a gallery id) copies its HTML and letter type; override html or letter_type as needed. - From scratch: pass html. HTML guidance (see spur://letter-design-guide): one US Letter page (8.5"×11"), full HTML document with inline <style>, "@page { size: 8.5in 11in; margin: 0; }", no scripts. One root <div class="letter"> 8.5in wide; px/in units only (no vh/vw); no box-shadow, transforms or manual page breaks. top_first_page: leave the first 3.15in blank (an empty <div class="address-zone" style="height:3.15in">), body ≈ 3–4 short paragraphs. insert_blank_page: no address zone; letterhead up to 1.8in, body up to ~400 words. Images as absolute https URLs. The user's own images (logo, headshot, property photos): spur_list_images gives their URLs; if they want to use an image they have, call spur_upload_image (or spur_save_image for a link) and use the saved URL. Use merge fields such as {{owner_first_name}}, {{property_address}}, {{your_name}}, {{your_phone}}, {{company_name}} (see spur://merge-variables).

spur_create_letter_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "name"
  ],
  "properties": {
    "html": {
      "type": "string",
      "maxLength": 500000,
      "description": "Full letter HTML"
    },
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "Template name shown in Spur"
    },
    "category": {
      "type": "string",
      "maxLength": 100,
      "description": "Category label, e.g. \"Seller Outreach\""
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Internal note about the template; never printed"
    },
    "letter_type": {
      "enum": [
        "top_first_page",
        "insert_blank_page"
      ],
      "type": "string",
      "description": "Defaults to the base template's type, else top_first_page"
    },
    "base_template_id": {
      "type": "string",
      "maxLength": 128,
      "minLength": 1,
      "description": "Copy this template's HTML and letter type as the starting point"
    },
    "start_from_blank": {
      "type": "boolean",
      "description": "Start from an empty page laid out for letter_type"
    }
  },
  "additionalProperties": false
}
spur_create_letter_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_update_letter_template — Edit letter template
Advertised by the server during the last successful anonymous check.

Edit one of the user's letter templates and show the updated preview. For text tweaks pass replacements (exact find → replace); to redesign, pass full html. letter_type switches between top_first_page and insert_blank_page. Spur default and gallery templates can't be edited: copy them first with spur_create_letter_template base_template_id. HTML guidance (see spur://letter-design-guide): one US Letter page (8.5"×11"), full HTML document with inline <style>, "@page { size: 8.5in 11in; margin: 0; }", no scripts. One root <div class="letter"> 8.5in wide; px/in units only (no vh/vw); no box-shadow, transforms or manual page breaks. top_first_page: leave the first 3.15in blank (an empty <div class="address-zone" style="height:3.15in">), body ≈ 3–4 short paragraphs. insert_blank_page: no address zone; letterhead up to 1.8in, body up to ~400 words. Images as absolute https URLs. The user's own images (logo, headshot, property photos): spur_list_images gives their URLs; if they want to use an image they have, call spur_upload_image (or spur_save_image for a link) and use the saved URL. Use merge fields such as {{owner_first_name}}, {{property_address}}, {{your_name}}, {{your_phone}}, {{company_name}} (see spur://merge-variables).

spur_update_letter_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id"
  ],
  "properties": {
    "html": {
      "type": "string",
      "maxLength": 500000,
      "description": "Full replacement HTML"
    },
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "New template name"
    },
    "category": {
      "type": "string",
      "maxLength": 100,
      "description": "Category label, e.g. \"Seller Outreach\""
    },
    "description": {
      "type": "string",
      "maxLength": 1000,
      "description": "Internal note about the template; never printed"
    },
    "letter_type": {
      "enum": [
        "top_first_page",
        "insert_blank_page"
      ],
      "type": "string",
      "description": "\"top_first_page\": the address prints in the top 3.15in of page 1 (shows through a #10 window envelope), so keep that band empty. \"insert_blank_page\": Spur adds a separate address page in front, so the letter can use the whole page (one extra page is billed)."
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Template id (UUID from a Spur list/search tool)"
    },
    "replacements": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "find",
          "replace"
        ],
        "properties": {
          "find": {
            "type": "string",
            "minLength": 1,
            "description": "Exact text that occurs once in the HTML"
          },
          "replace": {
            "type": "string",
            "description": "Text to put in its place"
          }
        },
        "additionalProperties": false
      },
      "maxItems": 30,
      "description": "Small edits applied in order instead of resending the full content"
    }
  },
  "additionalProperties": false
}
spur_update_letter_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_delete_letter_template — Delete letter template
Advertised by the server during the last successful anonymous check.
Advertised destructive

Permanently delete one of the user's letter templates. Spur defaults can't be deleted. Confirm with the user first.

spur_delete_letter_template input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Template id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_delete_letter_template annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": true
}
spur_preview_letter_campaign — Preview letter mailing
Advertised by the server during the last successful anonymous check.
Advertised read-only

Required step before mailing letters. Renders the letter with the first recipient's real data in its letter type's layout, checks which owners have mailing addresses, estimates the cost from the user's plan (Insert Blank Page letters include the extra address page), and returns a confirmation_token. Show the preview, then ask the user to approve. Only after they approve, call spur_send_letters with the token (the widget's Send button does the same). The token expires in 30 minutes and only covers this exact template, letter type, print options, recipient set and return address.

spur_preview_letter_campaign input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "template_id",
    "owner_ids"
  ],
  "properties": {
    "color": {
      "type": "boolean",
      "default": true,
      "description": "Print in color (false for black and white)"
    },
    "owner_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 1000,
      "minItems": 1,
      "description": "Owners to mail (spur_find_owners)"
    },
    "template_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Letter template id (UUID from a Spur list/search tool)"
    },
    "double_sided": {
      "type": "boolean",
      "default": false,
      "description": "Print on both sides of each sheet"
    },
    "return_address_id": {
      "type": "string",
      "description": "Defaults to the user's default return address"
    }
  },
  "additionalProperties": false
}
spur_preview_letter_campaign annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_send_letters — Send letters
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Mail the letters approved in spur_preview_letter_campaign. Irreversible and charges the user's account. Requires the preview's confirmation_token and the user's explicit approval (confirm=true).

spur_send_letters input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "confirmation_token",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "confirmation_token": {
      "type": "string",
      "minLength": 20,
      "description": "confirmation_token from spur_preview_letter_campaign"
    }
  },
  "additionalProperties": false
}
spur_send_letters annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_get_billing_summary — Billing summary
Advertised by the server during the last successful anonymous check.
Advertised read-only

Plan, wallet balance, and this period's usage and remaining allowance per feature (searches, enrichment, SMS, postcards, letters, AI). Check this before large sends.

spur_get_billing_summary input schema
{
  "type": "object",
  "properties": {}
}
spur_get_billing_summary annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_list_campaigns — List campaigns
Advertised by the server during the last successful anonymous check.
Advertised read-only

The user's campaigns (bulk email, SMS, direct mail, outreach) with status and recipient progress.

spur_list_campaigns input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "limit": {
      "type": "integer",
      "default": 25,
      "maximum": 100,
      "minimum": 1,
      "description": "Maximum campaigns to return"
    },
    "offset": {
      "type": "integer",
      "default": 0,
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Campaigns to skip, for paging"
    },
    "status": {
      "enum": [
        "draft",
        "active",
        "paused",
        "completed",
        "cancelled"
      ],
      "type": "string",
      "description": "Only campaigns with this status"
    }
  },
  "additionalProperties": false
}
spur_list_campaigns annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_campaign_stats — Campaign stats
Advertised by the server during the last successful anonymous check.
Advertised read-only

Delivery and run statistics for one campaign.

spur_get_campaign_stats input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "campaign_id"
  ],
  "properties": {
    "campaign_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Campaign id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_get_campaign_stats annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_direct_mail_analytics — Direct mail analytics
Advertised by the server during the last successful anonymous check.
Advertised read-only

Letters and postcards sent, delivered, in transit and returned, plus USPS tracking totals and spend.

spur_get_direct_mail_analytics input schema
{
  "type": "object",
  "properties": {}
}
spur_get_direct_mail_analytics annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_show_billing_options — Top up or upgrade
Advertised by the server during the last successful anonymous check.
Advertised read-only

Shows the user's wallet balance, one-click top-up amounts, auto-recharge status and every Spur plan with prices, in a widget with Pay/Upgrade buttons. Use whenever the user wants to add funds, buy credits, upgrade, change or compare plans, or a send failed for billing reasons.

spur_show_billing_options input schema
{
  "type": "object",
  "properties": {}
}
spur_show_billing_options annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_top_up_wallet — Top up wallet
Advertised by the server during the last successful anonymous check.
Open-world

Creates a Stripe checkout link to add funds to the user's Spur wallet (pays for postcards, letters, SMS, enrichment and other usage beyond the plan). No money moves until the user pays on Stripe, so no confirmation is needed. If the user didn't name an amount, suggest $25, $50 or $100, or enough to cover the send they're planning.

spur_top_up_wallet input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "amount_usd"
  ],
  "properties": {
    "amount_usd": {
      "type": "number",
      "maximum": 10000,
      "minimum": 5,
      "description": "Amount to add in US dollars (minimum $5)"
    }
  },
  "additionalProperties": false
}
spur_top_up_wallet annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_change_plan — Upgrade or change plan
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Moves the user to another Spur plan (ids from spur_show_billing_options; usually agent, team or brokerage). - No paid subscription yet: returns a Stripe checkout link. No confirmation needed. - Already subscribed: switches the plan immediately and charges or credits the prorated difference to the saved card. Call once without confirm to get the exact change, ask the user to approve it, then call again with confirm=true. - Moving to the free Solo plan returns a billing-portal link where the user cancels the paid subscription.

spur_change_plan input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "plan_id"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "description": "Only for users who already have a paid subscription: true ONLY after they approved the switch and the prorated charge in the conversation."
    },
    "plan_id": {
      "type": "string",
      "maxLength": 40,
      "minLength": 1,
      "description": "Target plan id, e.g. agent, team, brokerage"
    },
    "billing_interval": {
      "enum": [
        "monthly",
        "annual"
      ],
      "type": "string",
      "default": "monthly",
      "description": "annual is cheaper per month"
    }
  },
  "additionalProperties": false
}
spur_change_plan annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_check_payment — Check payment
Advertised by the server during the last successful anonymous check.

After the user pays a Stripe checkout link from spur_top_up_wallet or spur_change_plan, confirms the payment went through and returns the new wallet balance and plan. Safe to call repeatedly.

spur_check_payment input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "session_id"
  ],
  "properties": {
    "session_id": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "session_id returned with the checkout link (starts with cs_)"
    }
  },
  "additionalProperties": false
}
spur_check_payment annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_manage_billing — Billing portal
Advertised by the server during the last successful anonymous check.
Open-world

Link to the user's Stripe billing portal: update the card on file, download invoices, change or cancel the subscription.

spur_manage_billing input schema
{
  "type": "object",
  "properties": {}
}
spur_manage_billing annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_set_auto_recharge — Auto-recharge
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Turns wallet auto-recharge on or off. When on, Spur charges the saved card amount_usd whenever the balance drops below threshold_usd, so sends never fail for lack of funds. Needs a saved card (the first spur_top_up_wallet payment saves one). Turning it on needs the user's approval of the amount and threshold (confirm=true); turning it off doesn't.

spur_set_auto_recharge input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "enabled"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "description": "Required to enable: true ONLY after the user approved automatic charges of this amount at this threshold."
    },
    "enabled": {
      "type": "boolean",
      "description": "true = turn auto-recharge on, false = off"
    },
    "amount_usd": {
      "type": "number",
      "maximum": 10000,
      "minimum": 5,
      "description": "Amount added per recharge (required when enabling, minimum $5)"
    },
    "threshold_usd": {
      "type": "number",
      "maximum": 10000,
      "minimum": 0,
      "description": "Recharge when the balance drops below this (required when enabling; lower than amount_usd)"
    }
  },
  "additionalProperties": false
}
spur_set_auto_recharge annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_list_search_automations — List search automations
Advertised by the server during the last successful anonymous check.
Advertised read-only

Scheduled property searches with their schedule, filters, and next/last run times.

spur_list_search_automations input schema
{
  "type": "object",
  "properties": {}
}
spur_list_search_automations annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_create_search_automation — Create search automation
Advertised by the server during the last successful anonymous check.

Schedule a recurring property search. Every run uses 1 search credit, or 2 when it makes more than 3 ZHomes calls (several event types, tax/sale-history events, furnished-only or description-based filters). New matches can be saved to a list and enriched automatically (auto_enrich spends skip-trace credits on every run, so confirm with the user). Examples: - "every Monday 8am Central, Dallas rentals under $2k" -> frequency="weekly", day_of_week=1, run_time="08:00", timezone="America/Chicago", location="Dallas, TX", max_amount=2000 - "last Friday of each month" -> frequency="monthly", day_of_week=5, week_of_month=-1 - "every morning, new pre-foreclosures and price drops in Houston, save to my Distress list" -> frequency="daily", run_time="07:00", location="Houston, TX", events=["preforeclosure","price_drop"], list_id=… Accepts the same advanced and event filters as spur_search_properties (min_amount / max_amount instead of min_price / max_price). Run the search once with spur_search_properties first so the user can check the results before scheduling. In automations each event fires once, on the run that first sees it, so the same property isn't saved twice; change events start reporting from the second run. "delisted" is only available here. Events (pass in `events`; a listing matches when ANY selected event fired, and is tagged with each): - Listing lifecycle: new_listing, coming_soon, coming_soon_active, price_drop, price_increase, pending, sold, back_on_market, delisted (automations only), open_house, rental_listed, rent_change, move_in_moved_up - Distress & motivation: preforeclosure, foreclosure, auction, bank_owned, distress_escalated, fsbo, agent_to_fsbo, stale_listing, new_construction - Valuation & tax: tax_assessment_posted, assessed_value_jump, tax_paid_dropped (weak), sale_recorded, zestimate_moved, equity_gap, price_below_assessed Direct events (price_drop, pending, sold, preforeclosure, fsbo, new_listing, …) work on the first search. Change events (price_increase, back_on_market, distress_escalated, agent_to_fsbo, rent_change, zestimate_moved, …) compare against what Spur saw on an earlier search of the same area, so the first search only records a baseline. Motivated-seller research usually combines price_drop, back_on_market, stale_listing, agent_to_fsbo and the distress stages.

spur_create_search_automation input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "name",
    "location",
    "frequency",
    "run_time"
  ],
  "properties": {
    "name": {
      "type": "string",
      "maxLength": 255,
      "minLength": 1,
      "description": "Automation name shown in Spur"
    },
    "events": {
      "type": "array",
      "items": {
        "enum": [
          "new_listing",
          "coming_soon",
          "coming_soon_active",
          "price_drop",
          "price_increase",
          "pending",
          "sold",
          "back_on_market",
          "delisted",
          "open_house",
          "rental_listed",
          "rent_change",
          "move_in_moved_up",
          "preforeclosure",
          "foreclosure",
          "auction",
          "bank_owned",
          "distress_escalated",
          "fsbo",
          "agent_to_fsbo",
          "stale_listing",
          "new_construction",
          "tax_assessment_posted",
          "assessed_value_jump",
          "tax_paid_dropped",
          "sale_recorded",
          "zestimate_moved",
          "equity_gap",
          "price_below_assessed"
        ],
        "type": "string"
      },
      "maxItems": 15,
      "minItems": 1,
      "description": "Only listings where any of these events happened; see the tool description"
    },
    "enabled": {
      "type": "boolean",
      "default": true,
      "description": "false = save it paused"
    },
    "list_id": {
      "type": "string",
      "description": "Save new matches into this property list"
    },
    "max_age": {
      "type": "integer",
      "maximum": 400,
      "minimum": 0,
      "description": "At most this many years since built"
    },
    "max_zip": {
      "type": "integer",
      "maximum": 99999,
      "minimum": 0,
      "description": "Highest ZIP code in a numeric ZIP range (with min_zip)"
    },
    "min_age": {
      "type": "integer",
      "maximum": 400,
      "minimum": 0,
      "description": "Years since built"
    },
    "min_zip": {
      "type": "integer",
      "maximum": 99999,
      "minimum": 0,
      "description": "Lowest ZIP code in a numeric ZIP range (with max_zip); prefer zip_list for specific ZIPs"
    },
    "ai_query": {
      "type": "string",
      "maxLength": 1500,
      "description": "Qualitative intent; each run re-ranks listings by description against it"
    },
    "has_pool": {
      "type": "boolean",
      "description": "true = only listings with a pool"
    },
    "has_view": {
      "type": "boolean",
      "description": "true = only listings with any view (use views on live search for a specific kind)"
    },
    "keywords": {
      "type": "string",
      "maxLength": 300,
      "description": "Comma-separated listing keywords, e.g. \"pool, granite\""
    },
    "location": {
      "type": "string",
      "maxLength": 200,
      "minLength": 2,
      "description": "City/state or ZIP, e.g. \"Dallas, TX\""
    },
    "max_beds": {
      "type": "integer",
      "maximum": 20,
      "minimum": 0,
      "description": "Maximum bedrooms"
    },
    "max_sqft": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Maximum living area in square feet"
    },
    "min_beds": {
      "type": "integer",
      "maximum": 20,
      "minimum": 0,
      "description": "Minimum bedrooms"
    },
    "min_sqft": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Minimum living area in square feet"
    },
    "run_time": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
      "description": "24h local time \"HH:MM\""
    },
    "timezone": {
      "type": "string",
      "default": "UTC",
      "description": "IANA time zone, e.g. \"America/Chicago\""
    },
    "zip_list": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^\\d{5}$"
      },
      "maxItems": 50,
      "minItems": 1,
      "description": "Keep only these ZIPs; set location to the city that contains them"
    },
    "auto_save": {
      "type": "boolean",
      "default": true,
      "description": "Save new matches to the user's properties"
    },
    "frequency": {
      "enum": [
        "daily",
        "weekdays",
        "weekly",
        "biweekly",
        "monthly"
      ],
      "type": "string",
      "description": "How often the search runs"
    },
    "home_type": {
      "enum": [
        "singleFamily",
        "condo,apartment",
        "townhome",
        "multiFamily",
        "manufactured",
        "land"
      ],
      "type": "string",
      "description": "Zillow home type code"
    },
    "max_baths": {
      "type": "number",
      "maximum": 20,
      "minimum": 0,
      "description": "Maximum bathrooms"
    },
    "min_baths": {
      "type": "number",
      "maximum": 20,
      "minimum": 0,
      "description": "Minimum bathrooms (half baths count as 0.5)"
    },
    "center_lat": {
      "type": "number",
      "maximum": 90,
      "minimum": -90,
      "description": "Radius search centre (with center_lng and radius_miles)"
    },
    "center_lng": {
      "type": "number",
      "maximum": 180,
      "minimum": -180,
      "description": "Radius search centre longitude (with center_lat and radius_miles)"
    },
    "has_garage": {
      "type": "boolean",
      "description": "true = only listings with a garage"
    },
    "max_amount": {
      "type": "number",
      "minimum": 0,
      "description": "Maximum price in USD (rent per month for forRent)"
    },
    "min_amount": {
      "type": "number",
      "minimum": 0,
      "description": "Minimum price in USD (rent per month for forRent)"
    },
    "min_equity": {
      "type": "number",
      "minimum": 0,
      "description": "Estimated equity in USD (Zestimate or price minus assessed value; rough)"
    },
    "auto_enrich": {
      "type": "boolean",
      "default": false,
      "description": "Skip-trace new matches on every run (spends enrichment credits)"
    },
    "day_of_week": {
      "type": "integer",
      "maximum": 6,
      "minimum": 0,
      "description": "0=Sunday … 6=Saturday; for weekly, biweekly, monthly"
    },
    "days_posted": {
      "type": "integer",
      "maximum": 3650,
      "minimum": 1,
      "description": "Only listings posted within this many days"
    },
    "only_vacant": {
      "type": "boolean",
      "description": "Likely vacant (heuristic: description keywords + long days on market)"
    },
    "listing_type": {
      "enum": [
        "fsba",
        "fsbo",
        "newConstruction",
        "foreclosure",
        "preforeclosure",
        "foreclosed",
        "auction"
      ],
      "type": "string",
      "description": "Seller / listing kind; for catching NEW entries into a distress stage prefer events"
    },
    "max_lot_size": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Square feet"
    },
    "min_lot_size": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Square feet (1 acre = 43,560)"
    },
    "radius_miles": {
      "type": "number",
      "maximum": 500,
      "minimum": 0,
      "description": "Keep only listings within this many miles of center_lat / center_lng"
    },
    "is_waterfront": {
      "type": "boolean",
      "description": "true = only waterfront listings"
    },
    "neighborhoods": {
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 80,
        "minLength": 1
      },
      "maxItems": 20,
      "minItems": 1,
      "description": "Neighborhood / area names matched against address and description"
    },
    "only_absentee": {
      "type": "boolean",
      "description": "Absentee-owned (owner mails elsewhere)"
    },
    "week_of_month": {
      "anyOf": [
        {
          "type": "number",
          "const": 1
        },
        {
          "type": "number",
          "const": 2
        },
        {
          "type": "number",
          "const": 3
        },
        {
          "type": "number",
          "const": 4
        },
        {
          "type": "number",
          "const": -1
        }
      ],
      "description": "Monthly only; -1 = last"
    },
    "listing_status": {
      "enum": [
        "forRent",
        "forSale",
        "sold"
      ],
      "type": "string",
      "default": "forRent",
      "description": "Which listings to search"
    },
    "max_year_built": {
      "type": "integer",
      "maximum": 2100,
      "minimum": 1700,
      "description": "Built in or before this year"
    },
    "min_year_built": {
      "type": "integer",
      "maximum": 2100,
      "minimum": 1700,
      "description": "Built in or after this year"
    },
    "ownership_type": {
      "enum": [
        "individual",
        "business",
        "any"
      ],
      "type": "string",
      "description": "business = LLC / corp / trust owners (heuristic)"
    },
    "event_stale_days": {
      "type": "integer",
      "maximum": 3650,
      "minimum": 1,
      "description": "Days on market that counts as stale (default 90)"
    },
    "is_price_reduced": {
      "type": "boolean",
      "description": "true = only listings whose asking price has been cut"
    },
    "event_within_days": {
      "type": "integer",
      "maximum": 365,
      "minimum": 1,
      "description": "Lookback window for events, in days (default 7)"
    },
    "max_days_on_market": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Listed at most this many days"
    },
    "min_days_on_market": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 0,
      "description": "Listed at least this many days (higher suggests a motivated seller)"
    },
    "max_ownership_years": {
      "type": "number",
      "maximum": 150,
      "minimum": 0,
      "description": "Owner has held the property at most this long"
    },
    "min_ownership_years": {
      "type": "number",
      "maximum": 150,
      "minimum": 0,
      "description": "Owner has held the property at least this long"
    },
    "event_min_change_pct": {
      "type": "number",
      "maximum": 100,
      "minimum": 0,
      "description": "Minimum % move for price / rent / Zestimate / assessment events"
    },
    "only_pre_foreclosure": {
      "type": "boolean",
      "description": "Distressed / pre-foreclosure (heuristic, includes description keywords)"
    },
    "event_min_equity_gap_pct": {
      "type": "number",
      "maximum": 1000,
      "minimum": 0,
      "description": "Minimum % Zestimate above last sale price for equity_gap (default 30)"
    }
  },
  "additionalProperties": false
}
spur_create_search_automation annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_run_search_automation — Run search automation now
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Run a search automation immediately instead of waiting for its schedule. Uses 1 search credit (2 if the run makes more than 3 ZHomes calls), and enrichment credits if auto_enrich is on.

spur_run_search_automation input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "automation_id"
  ],
  "properties": {
    "automation_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Automation id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_run_search_automation annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_get_automation_logs — Automation run history
Advertised by the server during the last successful anonymous check.
Advertised read-only

Recent runs of a search automation: when, how many found/saved/enriched, and errors.

spur_get_automation_logs input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "automation_id"
  ],
  "properties": {
    "limit": {
      "type": "integer",
      "default": 10,
      "maximum": 50,
      "minimum": 1,
      "description": "Maximum runs to return"
    },
    "automation_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "Automation id (UUID from a Spur list/search tool)"
    }
  },
  "additionalProperties": false
}
spur_get_automation_logs annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_get_texting_setup — Texting setup status
Advertised by the server during the last successful anonymous check.
Advertised read-only

The user's texting setup: phone numbers bought in Spur (and which can send), carrier (A2P 10DLC) registration status, carrier notes, what's still missing, and the next step. Call this before helping with numbers or registration.

spur_get_texting_setup input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "refresh": {
      "type": "boolean",
      "default": false,
      "description": "true = ask the carriers for the latest status first (use when the user is waiting on approval)"
    }
  },
  "additionalProperties": false
}
spur_get_texting_setup annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_search_phone_numbers — Find phone numbers
Advertised by the server during the last successful anonymous check.
Advertised read-only

Search local US numbers the user can buy, by area code, state, city or digits. Show the options and let the user pick before spur_buy_phone_number. Prefer an area code in the user's market.

spur_search_phone_numbers input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "city": {
      "type": "string",
      "maxLength": 80
    },
    "limit": {
      "type": "integer",
      "default": 10,
      "maximum": 30,
      "minimum": 1
    },
    "state": {
      "type": "string",
      "pattern": "^[A-Za-z]{2}$",
      "description": "e.g. TX"
    },
    "contains": {
      "type": "string",
      "maxLength": 10,
      "minLength": 2,
      "description": "Digits the number should contain, e.g. 777"
    },
    "area_code": {
      "type": "string",
      "pattern": "^\\d{3}$",
      "description": "e.g. 512"
    }
  },
  "additionalProperties": false
}
spur_search_phone_numbers annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_buy_phone_number — Buy phone number
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Buy a local number for the user's account (it's theirs immediately and replies land in their SMS inbox). It can't send texts until their carrier registration is approved. Plans include some numbers; beyond that each costs $5/mo from the Spur wallet, first month charged now (spur_get_texting_setup shows the user's price). Tell the user the price and get explicit approval for this exact number first.

spur_buy_phone_number input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "phone_number",
    "confirm"
  ],
  "properties": {
    "region": {
      "type": "string",
      "maxLength": 8,
      "description": "State from the search result, for display"
    },
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "locality": {
      "type": "string",
      "maxLength": 80,
      "description": "City from the search result, for display"
    },
    "phone_number": {
      "type": "string",
      "pattern": "^\\+1\\d{10}$",
      "description": "phone_number from spur_search_phone_numbers"
    }
  },
  "additionalProperties": false
}
spur_buy_phone_number annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_set_default_phone_number — Set default number
Advertised by the server during the last successful anonymous check.

Make one of the user's bought numbers the default sender for their texts.

spur_set_default_phone_number input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "number_id"
  ],
  "properties": {
    "number_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "number_id from spur_get_texting_setup"
    }
  },
  "additionalProperties": false
}
spur_set_default_phone_number annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
spur_release_phone_number — Release phone number
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Permanently give up one of the user's bought numbers. They may never get it back, and contacts texting it will no longer reach them. Confirm with the user first.

spur_release_phone_number input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "number_id",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "number_id": {
      "type": "string",
      "maxLength": 64,
      "minLength": 1,
      "description": "number_id from spur_get_texting_setup"
    }
  },
  "additionalProperties": false
}
spur_release_phone_number annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": true,
  "destructiveHint": true
}
spur_save_texting_registration — Save texting registration
Advertised by the server during the last successful anonymous check.

Create or update the user's carrier (A2P 10DLC) registration draft. Pass only the fields to change; nested objects merge. Pick registration_type from two questions: has an EIN? (no → sole_proprietor) and more than ~2,000 texts/day? (yes → standard, else low_volume_standard). Business details must match IRS records exactly. Only record consent methods the user actually uses: carriers reject texting purchased or skip-traced lists without consent. write_campaign_text=true then reads their website and drafts the campaign description, opt-in flow and sample messages (needs message_types and consent_channels). Show that text to the user to review.

spur_save_texting_registration input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "address": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "maxLength": 255
        },
        "line1": {
          "type": "string",
          "maxLength": 255,
          "description": "Street address (no PO boxes)"
        },
        "line2": {
          "type": "string",
          "maxLength": 255
        },
        "region": {
          "type": "string",
          "maxLength": 2,
          "description": "2-letter state/province code, e.g. TX"
        },
        "country": {
          "enum": [
            "US",
            "CA"
          ],
          "type": "string"
        },
        "postal_code": {
          "type": "string",
          "maxLength": 10
        }
      },
      "additionalProperties": false
    },
    "contact": {
      "type": "object",
      "properties": {
        "email": {
          "type": "string",
          "maxLength": 255
        },
        "phone": {
          "type": "string",
          "maxLength": 20,
          "description": "US/Canadian number. For sole proprietors: their personal mobile, never a Spur number"
        },
        "job_title": {
          "type": "string",
          "maxLength": 255
        },
        "last_name": {
          "type": "string",
          "maxLength": 255
        },
        "first_name": {
          "type": "string",
          "maxLength": 255
        },
        "job_position": {
          "enum": [
            "CEO",
            "GM",
            "VP",
            "Director",
            "CFO",
            "General Counsel",
            "Other"
          ],
          "type": "string"
        }
      },
      "additionalProperties": false
    },
    "messaging": {
      "type": "object",
      "properties": {
        "terms_url": {
          "type": "string",
          "maxLength": 500
        },
        "description": {
          "type": "string",
          "maxLength": 4096,
          "description": "Campaign description (40+ chars)"
        },
        "message_flow": {
          "type": "string",
          "maxLength": 2048,
          "description": "How people opt in (40+ chars)"
        },
        "message_types": {
          "type": "array",
          "items": {
            "enum": [
              "conversations",
              "appointments",
              "deal_updates",
              "marketing"
            ],
            "type": "string"
          },
          "description": "What they text about"
        },
        "message_samples": {
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 1024
          },
          "maxItems": 5,
          "description": "2–5 samples naming the business; one must include 'Reply STOP to opt out'"
        },
        "opt_in_form_url": {
          "type": "string",
          "maxLength": 500,
          "description": "Required with website_form: the page with the SMS consent checkbox"
        },
        "consent_channels": {
          "type": "array",
          "items": {
            "enum": [
              "website_form",
              "inbound_text",
              "verbal",
              "paper_form"
            ],
            "type": "string"
          },
          "description": "How contacts agree to receive texts. Only what the user actually does; never invent consent"
        },
        "privacy_policy_url": {
          "type": "string",
          "maxLength": 500
        }
      },
      "additionalProperties": false
    },
    "business_info": {
      "type": "object",
      "properties": {
        "ein": {
          "type": "string",
          "maxLength": 20,
          "description": "9-digit EIN, e.g. 12-3456789"
        },
        "industry": {
          "enum": [
            "REAL_ESTATE",
            "FINANCIAL",
            "INSURANCE",
            "LEGAL",
            "CONSUMER",
            "ENGINEERING",
            "RETAIL",
            "TECHNOLOGY",
            "HOSPITALITY",
            "HEALTHCARE",
            "EDUCATION",
            "AUTOMOTIVE",
            "MANUFACTURING",
            "TRANSPORTATION",
            "MEDIA",
            "ONLINE",
            "NOT_FOR_PROFIT",
            "GOVERNMENT"
          ],
          "type": "string"
        },
        "brand_name": {
          "type": "string",
          "maxLength": 255,
          "description": "Name contacts know them by (DBA). Required for sole proprietors"
        },
        "legal_name": {
          "type": "string",
          "maxLength": 255,
          "description": "Legal business name exactly as on the IRS EIN letter, including LLC/Inc"
        },
        "website_url": {
          "type": "string",
          "maxLength": 500,
          "description": "Live website with a privacy policy; carriers visit it"
        },
        "company_type": {
          "enum": [
            "private",
            "public",
            "non-profit",
            "government"
          ],
          "type": "string"
        },
        "stock_ticker": {
          "type": "string",
          "maxLength": 10
        },
        "business_type": {
          "enum": [
            "Limited Liability Corporation",
            "Corporation",
            "Partnership",
            "Sole Proprietorship",
            "Non-profit Corporation",
            "Co-operative"
          ],
          "type": "string"
        },
        "stock_exchange": {
          "type": "string",
          "maxLength": 10
        }
      },
      "additionalProperties": false
    },
    "registration_type": {
      "enum": [
        "low_volume_standard",
        "standard",
        "sole_proprietor"
      ],
      "type": "string"
    },
    "write_campaign_text": {
      "type": "boolean",
      "default": false,
      "description": "true = also draft the campaign text from their website and answers"
    }
  },
  "additionalProperties": false
}
spur_save_texting_registration annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
spur_submit_texting_registration — Submit texting registration
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Send the user's registration to the carriers (also resubmits a rejected one after fixes). Charges the Spur wallet: $29 one-time for low volume / sole proprietor or $79 for standard (includes the first month, then $3 or $15/mo), or $15 to resubmit after a carrier rejection; spur_get_texting_setup shows the exact amount due now. Tell the user the amount. Walk the user through the full details first, and only call after they explicitly approve and personally confirm both statements: they only text people who agreed to receive their messages and honor STOP, and they won't send prohibited content (cannabis, firearms, high-risk loans or debt collection, gambling, sexual content, hate speech).

spur_submit_texting_registration input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "user_confirms_consent",
    "user_confirms_no_prohibited_content",
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    },
    "user_confirms_consent": {
      "type": "boolean",
      "const": true,
      "description": "The user confirmed they only text people who agreed to receive their messages and honor STOP requests"
    },
    "user_confirms_no_prohibited_content": {
      "type": "boolean",
      "const": true,
      "description": "The user confirmed they won't send prohibited content"
    }
  },
  "additionalProperties": false
}
spur_submit_texting_registration annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_pay_texting_fees — Pay overdue texting fees
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Retry overdue monthly number and registration fees from the Spur wallet right away (normally retried daily). Use after the user adds funds. Restores paused numbers once paid. Charges the wallet: confirm the amount with the user first.

spur_pay_texting_fees input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "confirm"
  ],
  "properties": {
    "confirm": {
      "type": "boolean",
      "const": true,
      "description": "Set to true ONLY after the user explicitly approved this exact action in the conversation. Never assume approval."
    }
  },
  "additionalProperties": false
}
spur_pay_texting_fees annotations
{
  "readOnlyHint": false,
  "openWorldHint": true,
  "idempotentHint": false,
  "destructiveHint": true
}
spur_resend_texting_verification — Resend verification text
Advertised by the server during the last successful anonymous check.

Sole proprietors only: re-send the carrier's verification text (the user replies YES) when the previous one expired after 24 hours.

spur_resend_texting_verification input schema
{
  "type": "object",
  "properties": {}
}
spur_resend_texting_verification annotations
{
  "readOnlyHint": false,
  "openWorldHint": false,
  "idempotentHint": false,
  "destructiveHint": false
}
search — Search Spur
Advertised by the server during the last successful anonymous check.
Advertised read-only

Search the user's saved Spur properties, owners and postcard templates by keyword (address, city, owner name, phone, email, template name). Returns ids for the fetch tool. For live listings use spur_search_properties.

search input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "query"
  ],
  "properties": {
    "query": {
      "type": "string",
      "maxLength": 200,
      "minLength": 1,
      "description": "Keywords: address, city, owner name, phone, email or template name"
    }
  }
}
search annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
fetch — Fetch Spur record
Advertised by the server during the last successful anonymous check.
Advertised read-only

Fetch the full text of a record returned by search (ids look like property:…, owner:… or template:…).

fetch input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "type": "string",
      "maxLength": 200,
      "minLength": 3,
      "description": "Record id exactly as returned by search, e.g. property:<uuid>"
    }
  }
}
fetch annotations
{
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}

Prompts

7 advertised

spur_deal_hunt — Deal hunt

Advertised by the server during the last successful anonymous check.

Find, shortlist and save properties in a market

location
City/state or ZIP, e.g. "Dallas, TX" (required)
criteria
e.g. "3+ bed under $2,000/mo, owned 10+ years"
spur_motivated_seller_research — Motivated seller research

Advertised by the server during the last successful anonymous check.

Find owners likely to sell using listing events (price drops, back on market, stale, distress, FSBO) and owner signals

location
City/state or ZIP, e.g. "Houston, TX" (required)
criteria
Extra constraints, e.g. "single family under $400k built before 1990"
days
Lookback window in days (default 14)
spur_follow_up_hot_leads — Follow up hot leads

Advertised by the server during the last successful anonymous check.

Triage SMS replies and draft follow-ups

No arguments advertised.

spur_postcard_campaign — Postcard campaign

Advertised by the server during the last successful anonymous check.

Pick or design a postcard, preview it with real owners, and mail it after approval

audience
Who to mail, e.g. "owners in my Austin Absentee list" or "all enriched owners in 78704" (required)
message
What the card should say or the vibe you want
spur_letter_campaign — Letter campaign

Advertised by the server during the last successful anonymous check.

Pick or design a letter (Top First Page or Insert Blank Page), preview it with real owners, and mail it after approval

audience
Who to mail, e.g. "absentee owners in 78704" (required)
message
What the letter should say or the tone you want
spur_outreach_sequence — Outreach sequence

Advertised by the server during the last successful anonymous check.

Design email and SMS templates, preview them, and assemble them into an outreach workflow

goal
What the sequence is for, e.g. "first touch to absentee owners offering a cash purchase" (required)
channels
e.g. "text, then email two days later, then a postcard"
spur_weekly_review — Weekly review

Advertised by the server during the last successful anonymous check.

Summarize the week: spend, campaigns, mail delivery and replies

No arguments advertised.

Resources

9 advertised

Spur property explorer

Advertised by the server during the last successful anonymous check.

Map and cards for Spur property search results and saved properties

Spur property explorer URI
ui://spur/property-explorer-v1.html

MIME type: text/html;profile=mcp-app

Spur postcard preview

Advertised by the server during the last successful anonymous check.

Postcard (front/back) and letter page preview with merge data, cost estimate and a send button

Spur postcard preview URI
ui://spur/postcard-preview-v2.html

MIME type: text/html;profile=mcp-app

Spur email and SMS preview

Advertised by the server during the last successful anonymous check.

Email inbox preview (desktop/mobile, HTML and plain text) and SMS phone preview with segment count and design checks

Spur email and SMS preview URI
ui://spur/message-preview-v1.html

MIME type: text/html;profile=mcp-app

Spur billing

Advertised by the server during the last successful anonymous check.

Wallet balance with one-click top-ups, plan cards with Upgrade buttons, and Stripe checkout links

Spur billing URI
ui://spur/billing-v1.html

MIME type: text/html;profile=mcp-app

Spur image upload

Advertised by the server during the last successful anonymous check.

Pick or drop an image from your device to save it to your Spur image library for postcards, letters and emails

Spur image upload URI
ui://spur/image-upload-v1.html

MIME type: text/html;profile=mcp-app

spur-merge-variables — Merge fields

Advertised by the server during the last successful anonymous check.

Every {{merge_field}} Spur fills in for postcards, letters, email and SMS

spur-merge-variables URI
spur://merge-variables

MIME type: text/markdown

spur-postcard-guide — Postcard design guide

Advertised by the server during the last successful anonymous check.

Sizes, bleed, address-block safe zone and HTML rules for Spur postcards

spur-postcard-guide URI
spur://postcard-design-guide

MIME type: text/markdown

spur-letter-guide — Letter design guide

Advertised by the server during the last successful anonymous check.

Letter types (Top First Page, Insert Blank Page), address area and HTML rules for Spur letters

spur-letter-guide URI
spur://letter-design-guide

MIME type: text/markdown

spur-email-guide — Email design guide

Advertised by the server during the last successful anonymous check.

Layout, client-safe HTML, preheader, footer compliance and the blank layout for Spur email templates

spur-email-guide URI
spur://email-design-guide

MIME type: text/markdown

30-day availability

Aggregated from read-only remote checks; this is not a tool-execution or security guarantee.

Successful checks
1 of 1 (100%)
Average latency
1245 ms
95th percentile latency
1245 ms

Maintenance history

Public check and source-refresh events. Editorial and ownership records remain private.

  • · probe · passed · 1245 ms · metadata changed · 87 tool(s), 9 resource(s), 0 resource template(s), 7 prompt(s)

Connect from your client

Each snippet registers the same endpoint; pick the client you use.

This server is operated by the named third party. Listing and endpoint checks are not an endorsement or a security audit. Review the operator's permissions, terms, and data handling before connecting. How listings are checked and corrected.

Claude Code

Run this once in a terminal. It registers the remote server. Use --scope project to write .mcp.json for the repo.
Claude Code configuration
claude mcp add --transport http spur-mcp https://mcp.usespur.com/mcp

Codex

Run this once. Codex stores MCP servers in ~/.codex/config.toml (or .codex/config.toml in a trusted project).
Codex configuration
codex mcp add spur-mcp --url https://mcp.usespur.com/mcp

Cursor

Merge this server into .cursor/mcp.json in your project, then enable spur-mcp in your MCP settings.
Cursor configuration
{
  "mcpServers": {
    "spur-mcp": {
      "url": "https://mcp.usespur.com/mcp"
    }
  }
}

Cline

Merge this into cline_mcp_settings.json (Cline → MCP Servers → Configure).
Cline configuration
{
  "mcpServers": {
    "spur-mcp": {
      "type": "streamableHttp",
      "url": "https://mcp.usespur.com/mcp",
      "disabled": false
    }
  }
}

VS Code

Merge this server into .vscode/mcp.json. Use MCP: List Servers to start spur-mcp and inspect its tools.
VS Code configuration
{
  "servers": {
    "spur-mcp": {
      "type": "http",
      "url": "https://mcp.usespur.com/mcp"
    }
  }
}

Gemini CLI

Run this once. It writes .gemini/settings.json in the current project; pass --scope user for ~/.gemini/settings.json.
Gemini CLI configuration
gemini mcp add --transport http spur-mcp https://mcp.usespur.com/mcp

Claude Desktop

Merge this into claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\), then restart Claude Desktop.
Claude Desktop configuration
{
  "mcpServers": {
    "spur-mcp": {
      "url": "https://mcp.usespur.com/mcp"
    }
  }
}

Windsurf

Merge this into ~/.codeium/windsurf/mcp_config.json, then refresh MCP in Cascade. Windsurf remote servers use serverUrl, not url.
Windsurf configuration
{
  "mcpServers": {
    "spur-mcp": {
      "serverUrl": "https://mcp.usespur.com/mcp"
    }
  }
}

Let your agent find its tools

Find servers like Spur MCP from your agent. Add Koskamo once to search the index for endpoints, access requirements, and client setup. Public discovery needs no key.

Do you run Spur MCP?

Prove ownership with a DNS record, a well-known file, or your Official MCP Registry entry, and your edits publish immediately. Proofs are re-checked weekly; admins can still hide or remove a listing.

Claim this listing