MCP Tools — Email, Surveys, Pitch Decks & Automations

These MCP tools let you manage your email marketing (lists, subscribers, campaigns, templates, segments), build and publish surveys and intake forms, create and iterate on pitch decks and presentations, and define automation rules — all from any AI client connected to the SimplerDevelopment portal.

Authentication and connection setup are covered in MCP Overview. Every tool call is scoped to your client account — you cannot read or write another tenant's data.


Approval workflow#

Many write tools in this area go through an approval step before they take effect. When a tool returns approval.url, share that URL with your team (or click it yourself) to approve or reject the pending change. Approving a campaign draft does NOT send it — approving only commits the draft to the database. Sending requires a separate, explicit call to email_campaigns_send.

When a write lands directly (no pending workflow), the response will not include a pending: true key — you'll get the created/updated row directly.


Email#

Required service: email

email_lists#

List all email marketing lists owned by your account.

  • Auth: scope email:read
  • Input: (none)

Response

[
  {
    "id": 12,
    "clientId": 4,
    "name": "Newsletter Subscribers",
    "description": "Main newsletter list",
    "createdAt": "2025-11-01T10:00:00.000Z",
    "updatedAt": "2025-11-01T10:00:00.000Z"
  }
]

Example

{ "tool": "email_lists" }

email_lists_create#

Create a new email list.

  • Auth: scope email:write

Input

FieldTypeDescription
namestring (required)List name.
descriptionstringOptional description.

Request body

{
  "name": "Q1 Prospects",
  "description": "Leads acquired in Q1 campaign"
}

Response

{
  "id": 13,
  "clientId": 4,
  "name": "Q1 Prospects",
  "description": "Leads acquired in Q1 campaign",
  "createdAt": "2026-01-10T09:00:00.000Z",
  "updatedAt": "2026-01-10T09:00:00.000Z"
}

email_lists_update#

Rename a list or update its description.

  • Auth: scope email:write

Input

FieldTypeDescription
idnumber (required)List ID.
namestringNew name.
descriptionstring | nullNew description (pass null to clear).

Response: Updated list row.

Errors: { "error": "List not found" }


email_lists_delete#

Permanently delete a list and all its subscribers. Blocked if campaigns reference the list.

  • Auth: scope email:write

Input

FieldTypeDescription
idnumber (required)List ID.

Response

{ "success": true, "id": 13 }

Errors: { "error": "Cannot delete: <db constraint message>" }


email_subscribers_list#

List subscribers on a list, newest first.

  • Auth: scope email:read

Input

FieldTypeDescription
listIdnumber (required)List to query.
statusactive | unsubscribed | bounced | complainedFilter by status.
searchstringCase-insensitive match on email or name.
limitnumber (1–500, default 100)Max rows to return.

Response

[
  {
    "id": 501,
    "listId": 12,
    "email": "jane@example.com",
    "name": "Jane Doe",
    "status": "active",
    "metadata": { "source": "webinar" },
    "subscribedAt": "2026-01-15T08:00:00.000Z",
    "unsubscribedAt": null
  }
]

Errors: { "error": "List not found" }


email_subscribers_add#

Add a subscriber to a list. If the email already exists on that list, the existing row is updated instead of creating a duplicate. A fresh unsubscribe token is generated for new rows.

  • Auth: scope email:write

Input

FieldTypeDescription
listIdnumber (required)Target list.
emailstring/email (required)Subscriber email (normalized to lowercase).
namestringDisplay name.
metadataRecord<string, string>Arbitrary key-value pairs.
statusactive | unsubscribed | bounced | complainedDefaults to active.

Response: The inserted or updated subscriber row.

Errors: { "error": "List not found" }


email_subscribers_update#

Update a subscriber's name, status, or metadata.

  • Auth: scope email:write

Input

FieldTypeDescription
idnumber (required)Subscriber ID.
namestring | nullDisplay name (pass null to clear).
statusactive | unsubscribed | bounced | complainedNew status. Setting unsubscribed stamps unsubscribedAt.
metadataRecord<string, string> | nullReplaces existing metadata (pass null to clear).

Response: Updated subscriber row.

Errors: { "error": "Subscriber not found" }


email_subscribers_remove#

Remove a subscriber. Default is a soft unsubscribe (status → unsubscribed). Pass hardDelete: true to permanently delete the row.

  • Auth: scope email:write

Input

FieldTypeDescription
idnumber (required)Subscriber ID.
hardDeletebooleanIf true, permanently deletes the row. Default false.

Response

{ "success": true, "id": 501, "mode": "soft" }

Errors: { "error": "Subscriber not found" }


email_campaigns_list#

List campaigns, newest first. Returns the slim projection (no HTML body or block JSON) by default.

  • Auth: scope email:read

Input

FieldTypeDescription
statusstringFilter by status (e.g. draft, scheduled, sent).
includeContentbooleanInclude htmlContent + blockContent. Default false — these can be hundreds of KB per row.

Response

[
  {
    "id": 88,
    "name": "March Newsletter",
    "subject": "What's new at Acme",
    "status": "draft",
    "listId": 12,
    "fromName": "Acme Team",
    "fromEmail": "hello@acme.com",
    "createdAt": "2026-03-01T10:00:00.000Z",
    "updatedAt": "2026-03-01T10:00:00.000Z"
  }
]

email_campaigns_create#

Create a draft email campaign tied to a list. Provide either htmlContent or blocks (rendered server-side to HTML). Campaign status starts as draft. Returns an approval.url for human review — approval does not send the campaign.

  • Auth: scope email:write

Input

FieldTypeDescription
namestring (required)Internal campaign name.
subjectstring (required)Email subject line.
listIdnumber (required)Target list (must belong to your account).
fromNamestring (required)Sender display name.
fromEmailstring/email (required)Sender address.
replyTostring/emailReply-to address.
previewTextstringEmail preview snippet.
htmlContentstringPre-rendered HTML body.
blocksarrayArray of Block objects — rendered to HTML server-side. Provide this OR htmlContent, not both.
includeContentbooleanEcho htmlContent + blockContent in the response. Default false.

Request body

{
  "name": "March Newsletter",
  "subject": "What's new this month",
  "listId": 12,
  "fromName": "Acme Team",
  "fromEmail": "hello@acme.com",
  "previewText": "Read our latest updates...",
  "htmlContent": "<p>Hello!</p>"
}

Response

{
  "id": 88,
  "name": "March Newsletter",
  "subject": "What's new this month",
  "status": "draft",
  "listId": 12,
  "approval": {
    "url": "https://app.simplerdevelopment.com/approve/abc123",
    "expiresAt": "2026-03-08T10:00:00.000Z"
  }
}

Errors: { "error": "List not found" }, { "error": "Provide htmlContent or non-empty blocks" }


email_campaigns_update#

Update metadata or content of a draft campaign. Refuses campaigns in sending, sent, or scheduled state (unschedule first with email_campaigns_schedule).

  • Auth: scope email:write

Input

FieldTypeDescription
idnumber (required)Campaign ID.
namestringNew internal name.
subjectstringNew subject line.
previewTextstring | nullPreview text (pass null to clear).
fromNamestringSender display name.
fromEmailstring/emailSender address.
replyTostring/email | nullReply-to address.
listIdnumberTarget list.
htmlContentstringReplacement HTML body.
blocksarrayBlock array — re-renders HTML if provided.

Response: Updated campaign slim projection + approval.url.

Errors: { "error": "Campaign not found" }, { "error": "Cannot edit — status is <status>" }, { "error": "Target list not found" }


email_campaigns_schedule#

Mark a draft campaign as scheduled for a future send. Sets statusscheduled and records scheduledAt. Pass unschedule: true to revert to draft. Does not dispatch the campaign — a scheduler or explicit email_campaigns_send call is still required.

  • Auth: scope email:write

Input

FieldTypeDescription
idnumber (required)Campaign ID.
scheduledAtstring (ISO datetime)Required unless unschedule: true. Must be in the future.
unschedulebooleanPass true to revert a scheduled campaign back to draft.

Errors: { "error": "scheduledAt must be in the future" }, { "error": "Cannot schedule — current status is <status>" }, { "error": "Cannot unschedule — current status is <status>" }


email_campaigns_send#

Dispatch a draft or scheduled campaign to every active subscriber on its list. Skips subscribers who have already received it (resume-safe). This call is synchronous — large lists will block the MCP call. Always run dryRun: true first to verify target counts.

Important: Sending requires the separate email:send scope, which should be granted explicitly and sparingly in addition to email:write.

  • Auth: scope email:send

Input

FieldTypeDescription
idnumber (required)Campaign ID.
dryRunbooleanIf true, returns target counts without sending anything.

Dry-run response

{
  "dryRun": true,
  "campaignId": 88,
  "listId": 12,
  "totalActive": 1200,
  "alreadySent": 0,
  "willSend": 1200
}

Live send response: send-result object from executeCampaignSend.

Errors: { "error": "Campaign not found" }, { "error": "Campaign is already <status>" } (status is sent or sending)


email_campaigns_fork#

Duplicate a campaign into a new draft, linked to the original via parentCampaignId. Send counts, status, and schedule metadata are not carried over — only editable content. Returns the new campaign ID and an approval.url.

  • Auth: scope email:write

Input

FieldTypeDescription
idnumber (required)Source campaign to fork.
nameSuffixstringAppended to the forked campaign's name. Default " (fork)".

Response

{
  "id": 91,
  "name": "March Newsletter (fork)",
  "status": "draft",
  "parentCampaignId": 88,
  "approval": { "url": "https://app.simplerdevelopment.com/approve/def456" }
}

Errors: { "error": "Source campaign not found" }


email_campaigns_delete#

Permanently delete a campaign. Blocked if the campaign is in sent or sending status.

  • Auth: scope email:write

Input

FieldTypeDescription
idnumber (required)Campaign ID.

Response: { "success": true, "id": 88 } or a pending approval object.

Errors: { "error": "Campaign not found" }, { "error": "Cannot delete a campaign in status <status>" } (status is sent or sending)


email_templates_list#

List reusable email templates (your account's templates plus global agency templates).

  • Auth: scope email:read

Input

FieldTypeDescription
categorywelcome | newsletter | promotion | transactional | customFilter by template category.

Response

[
  {
    "id": 5,
    "name": "Welcome Email",
    "description": "Sent to new subscribers",
    "category": "welcome",
    "subject": "Welcome aboard!",
    "thumbnailUrl": null,
    "isGlobal": true,
    "usageCount": 42,
    "updatedAt": "2026-01-01T00:00:00.000Z"
  }
]

email_templates_create#

Save a reusable email template. Provide htmlContent or blocks.

  • Auth: scope email:write

Input

FieldTypeDescription
namestring (required)Template name.
categorywelcome | newsletter | promotion | transactional | customDefaults to custom.
subjectstringDefault subject line.
descriptionstringShort description.
htmlContentstringHTML body.
blocksarrayBlock array (rendered to HTML). Provide this OR htmlContent.

Response: Template row (id, name, category, subject, description, thumbnailUrl, isGlobal, usageCount, createdAt, updatedAt).

Errors: { "error": "Provide htmlContent or non-empty blocks" }


email_segments_list#

List rule-based subscriber segment definitions.

  • Auth: scope email:read
  • Input: (none)

Response

[
  {
    "id": 3,
    "name": "High-engagement",
    "description": null,
    "matchType": "all",
    "rules": [{ "field": "openRate", "operator": "gt", "value": "0.5" }],
    "updatedAt": "2026-02-01T00:00:00.000Z"
  }
]

email_segments_create#

Define a subscriber segment by filter rules.

  • Auth: scope email:write

Input

FieldTypeDescription
namestring (required)Segment name.
descriptionstringOptional description.
matchTypeall | anyall = AND all rules; any = OR. Defaults to all.
rulesarray (required)Each rule: { field: string, operator: string, value: string }.

Request body

{
  "name": "Openers only",
  "matchType": "all",
  "rules": [
    { "field": "openRate", "operator": "gt", "value": "0" }
  ]
}

Response: New segment row.


email_analytics_get#

Return lifetime campaign performance aggregates for this client's email programme: campaigns sent, total recipients, opens, clicks, bounces, unsubscribes, open rate, click rate, and list count. Totals are across all sent campaigns (not date-windowed); the optional days parameter is accepted for forward-compatibility but has no effect.

  • Auth: scope email:read

Input fields:

FieldTypeRequiredDescription
daysnumberNoAccepted for forward-compatibility; currently unused — totals are lifetime.

Response:

{
  "totalCampaigns": 12,
  "totalSent": 14800,
  "totalOpened": 5920,
  "totalClicked": 1184,
  "totalBounced": 74,
  "totalUnsubscribed": 30,
  "openRate": "40.0",
  "clickRate": "8.0",
  "totalLists": 3
}

openRate and clickRate are percentage strings with one decimal place (e.g. "40.0" = 40%).


Surveys#

Required service: surveys

surveys_list#

List surveys (forms, intake questionnaires, feedback polls), newest-first.

  • Auth: scope surveys:read

Input

FieldTypeDescription
statusdraft | active | closedFilter by status.
limitnumber (1–200, default 50)Max rows to return.

Response

[
  {
    "id": 7,
    "title": "Client Intake Form",
    "slug": "client-intake-form-l3x9k",
    "description": null,
    "status": "active",
    "responseCount": 34,
    "closesAt": null,
    "createdAt": "2026-01-05T00:00:00.000Z",
    "updatedAt": "2026-02-10T00:00:00.000Z"
  }
]

surveys_get#

Fetch a survey's full definition including all fields, pages, settings, and scoring config.

  • Auth: scope surveys:read

Input

FieldTypeDescription
idnumber (required)Survey ID.

Response: Full survey row (all columns).

Errors: { "error": "Survey not found" }


surveys_list_responses#

List submitted responses for a survey. Answers are a JSON object keyed by field ID.

  • Auth: scope surveys:read

Input

FieldTypeDescription
surveyIdnumber (required)Survey to query.
sincestring (ISO date)Return responses submitted after this timestamp.
limitnumber (1–500, default 100)Max rows.

Response

[
  {
    "id": 201,
    "surveyId": 7,
    "answers": { "field_1": "Jane Doe", "field_2": "jane@example.com" },
    "createdAt": "2026-03-12T14:22:00.000Z"
  }
]

Errors: { "error": "Survey not found" }


surveys_create#

Create a new survey. Survey starts in draft status — activate it with surveys_update. Returns an approval.url you can share; approving in the portal flips statusactive so the public /s/<slug> route accepts responses.

  • Auth: scope surveys:write

Input

FieldTypeDescription
titlestring (required)Survey title.
descriptionstringIntro text shown to respondents.
fieldsarraySurveyFieldDef[] — see below.
thankYouTitlestringHeading shown after submit.
thankYouMessagestringBody text shown after submit.
requireEmailbooleanRequire respondents to provide an email. Default false.
allowMultiplebooleanAllow the same person to respond more than once. Default true.

SurveyFieldDef shape (each field in the fields array):

{
  "id": "field_1",
  "type": "text",
  "label": "Full name",
  "required": true,
  "order": 1
}

Supported type values: text, textarea, email, phone, select, radio, checkbox, toggle, date, rating, number, url, heading, slider.

Response

{
  "id": 8,
  "title": "New Client Intake",
  "slug": "new-client-intake-m4y2z",
  "status": "draft",
  "approval": {
    "url": "https://app.simplerdevelopment.com/approve/ghi789"
  }
}

surveys_update#

Update any combination of survey fields. Only pass what you want to change — unspecified fields are left as-is. Mints a fresh approval.url on every call.

  • Auth: scope surveys:write

Input

FieldTypeDescription
idnumber (required)Survey ID.
titlestringNew title.
descriptionstring | nullNew description.
statusdraft | active | closedChange status directly (e.g. "active" to publish).
fieldsarrayFull replacement SurveyFieldDef[].
thankYouTitlestringThank-you heading.
thankYouMessagestringThank-you body.
closesAtstring (ISO) | nullAuto-close deadline (pass null to remove).
maxResponsesnumber | nullResponse cap (pass null to remove).
brandingProfileIdnumber | nullApply a branding profile.
stylingobjectSurveyStyling{ primaryColor?, backgroundColor?, textColor?, headingFont?, bodyFont?, borderRadius?, showLogo?, hideTitle? }.
colorstringLegacy single-color hex override. Prefer styling.primaryColor.
pagesarrayPer-page metadata: [{ title?, description? }]. Page boundaries are inferred from type: "page_break" fields.
publishResultsbooleanMake aggregate results publicly visible.
certificateEnabledbooleanIssue a completion certificate.
consentFieldstring | nullField ID that gates submission via an explicit consent checkbox.
notifyOnResponsebooleanNotify account on each new response.
notifyDigestoff | daily | weeklyResponse digest emails.
scoringConfigobjectSurveyScoringConfig{ autoRouteToCrm?: { enabled, minScore, pipelineId, stageId, dealTitleTemplate? } }.
recommendationobjectSurveyRecommendationConfig{ offerings[], questions[], overrides[], hybrid?, alwaysAlsoOfferingKey?, bookUrl?, narrativeTemplate? }.
linkedTypeemail_campaign | crm_deal | crm_proposal | booking_page | website | pitch_deck | nullLink to another artifact.
linkedIdnumber | nullID of the linked artifact.
redirectUrlstring | nullSend respondents to this URL after submit (overrides the thank-you screen).

Response: Updated survey row + fresh approval.url.

Errors: { "error": "Survey not found" }


surveys_fork#

Duplicate a survey into a new draft row linked to the original via parentSurveyId. Copies fields, branding, styling, scoring, recommendation config, and thank-you copy. Status resets to draft and responseCount resets to 0. The fork gets its own slug and approval URL — the original is untouched.

  • Auth: scope surveys:write

Input

FieldTypeDescription
idnumber (required)Source survey to fork.
titleSuffixstringAppended to the cloned title. Default " (fork)".

Response

{
  "id": 9,
  "title": "Client Intake (fork)",
  "slug": "client-intake-form-fork-n5a1b",
  "status": "draft",
  "parentSurveyId": 7,
  "approval": { "url": "https://app.simplerdevelopment.com/approve/jkl012" }
}

Errors: { "error": "Source survey not found" }


Pitch Decks#

Required service: pitch-decks

Slides use a draft/live system. Write operations (decks_replace_slides, decks_add_slide, decks_upload_html) stage changes into slide.draft. The public renderer keeps showing the previous live slides until you call decks_publish_slide or decks_publish_all.

decks_list#

List pitch decks (presentations, slideshows, sales decks), newest-first.

  • Auth: scope decks:read

Input

FieldTypeDescription
statusdraft | published | archivedFilter by status.
limitnumber (1–200, default 50)Max rows.

Response

[
  {
    "id": 3,
    "title": "Q1 Sales Deck",
    "slug": "q1-sales-deck-p7r2m",
    "description": null,
    "status": "published",
    "formatVersion": 2,
    "brandingProfileId": 1,
    "createdAt": "2026-01-20T00:00:00.000Z",
    "updatedAt": "2026-02-14T00:00:00.000Z"
  }
]

decks_get#

Fetch a deck's full definition including slides, theme, and all metadata.

  • Auth: scope decks:read

Input

FieldTypeDescription
idnumber (required)Deck ID.

Response: Full deck row (all columns including slides array).

Errors: { "error": "Deck not found" }


decks_create#

Create a new empty pitch deck. The deck inherits the client's default branding profile automatically — do not pass theme unless you specifically want to override brand colors. Follow immediately with decks_replace_slides or decks_add_slide. Returns an approval.url.

  • Auth: scope decks:write

Input

FieldTypeDescription
titlestring (required)Deck title.
descriptionstringOptional description.
sourceUrlstring (URL)Reference site URL for branding inspiration.
brandingProfileIdnumberOverride the auto-resolved default branding profile.
themeobjectOverride specific theme tokens: { primaryColor?, accentColor?, backgroundColor?, textColor?, headingFont?, bodyFont?, logo? }.
includeSlidesbooleanEcho slides in the response. Default false.

Response

{
  "id": 4,
  "title": "Investor Deck 2026",
  "slug": "investor-deck-2026-q8s3n",
  "status": "draft",
  "formatVersion": 2,
  "approval": {
    "url": "https://app.simplerdevelopment.com/approve/mno345"
  }
}

Errors: { "error": "Branding profile not found for this client" }


decks_update#

Update deck metadata or theme. For slide content use decks_replace_slides or decks_add_slide.

  • Auth: scope decks:write

Input

FieldTypeDescription
idnumber (required)Deck ID.
titlestringNew title.
descriptionstringNew description.
statusdraft | published | archivedNew status.
slugstringNew URL slug.
themeobjectPartial theme override (merged with existing).
includeSlidesbooleanEcho slides in response. Default false.

Response: Updated deck slim projection + approval.url.

Errors: { "error": "Deck not found" }


decks_replace_slides#

Replace the entire slide array with a new list. Changes land in slide drafts — the public renderer continues showing current live slides until you publish. Existing slides matched by id get their draft updated; new IDs become pendingCreate drafts; slides missing from the incoming list become pendingDelete tombstones.

  • Auth: scope decks:write

Input

FieldTypeDescription
idnumber (required)Deck ID.
slidesarray (required)Full new slide list (see shape below).
includeSlidesbooleanEcho slides in response. Default false.

Slide shape:

{
  "id": "slide-abc",
  "label": "Problem",
  "blocks": [ /* Block objects — see blocks://schema */ ],
  "notes": "Speaker notes here",
  "customCss": ".hero { background: #1e1e2e; }",
  "pageSettings": { "backgroundColor": "#1e1e2e" }
}

Response: Deck slim projection (no slides by default).

Errors: { "error": "Deck not found" }


decks_add_slide#

Append a single slide to the end of a deck. The new slide lands in draft (pendingCreate: true) until published.

  • Auth: scope decks:write

Input

FieldTypeDescription
deckIdnumber (required)Target deck.
labelstring (required)Slide name shown in sidebar (e.g. "Cover", "Problem").
blocksarray (required)Block objects.
notesstringSpeaker notes.
pageSettingsobjectPage-level settings (e.g. backgroundColor, padding).
customCssstringPer-slide CSS scoped to this slide.
idstringExplicit slide ID; auto-generated if omitted.
includeSlidesbooleanEcho the full slides array in response. Default false.

Response: Deck slim projection.

Errors: { "error": "Deck not found" }


decks_publish_slide#

Promote a single slide's draft to live. If draft.pendingDelete: true the slide is removed; otherwise draft.blocks/customCss/pageSettings/notes are copied to the live fields and draft is cleared.

  • Auth: scope decks:write

Input

FieldTypeDescription
deckIdnumber (required)Deck ID.
slideIdstring (required)Slide ID to publish.

Response: Deck slim projection.

Errors: { "error": "Deck not found" }, { "error": "Slide not found" }


decks_publish_all#

Publish all draft slides on a deck in one call. Removes pendingDelete tombstones, materializes pendingCreate slides, and merges regular update drafts into live fields.

  • Auth: scope decks:write

Input

FieldTypeDescription
deckIdnumber (required)Deck ID.

Response: Deck slim projection.

Errors: { "error": "Deck not found" }


decks_fork#

Duplicate a deck into a new draft, linked to the original via parentDeckId. Copies slides, theme, and metadata. Status resets to draft.

  • Auth: scope decks:write

Input

FieldTypeDescription
idnumber (required)Source deck to fork.
titleSuffixstringAppended to the forked deck's title. Default " (fork)".

Response

{
  "id": 5,
  "title": "Investor Deck 2026 (fork)",
  "slug": "investor-deck-2026-fork-r9t4p",
  "status": "draft",
  "parentDeckId": 4,
  "approval": { "url": "https://app.simplerdevelopment.com/approve/pqr678" }
}

Errors: { "error": "Source deck not found" }


decks_upload_html#

Upload a single HTML file (base64-encoded) as a single-slide pitch deck wrapping an html-embed block. The slide counter is suppressed for full-bleed presentation. Max 1 MB decoded. The slide lands in draft (pendingCreate); call decks_publish_slide or decks_publish_all to make it live.

  • Auth: scope decks:write

Input

FieldTypeDescription
filenamestring (required)Must end in .html, .htm, or .xhtml.
contentBase64string (required)Base64-encoded HTML. Decoded size must be ≤ 1 MB.
titlestringDeck title override; defaults to the filename without extension.

Errors: { "error": "Invalid base64 content" }, { "error": "File exceeds 1000000 bytes" }, { "error": "Empty file" }


decks_upload_html_zip#

Upload a zip bundle (base64-encoded) containing index.html plus supporting assets as a single-slide pitch deck. All files are uploaded to a shared S3 prefix; relative asset references from index.html resolve through the media proxy. Max 50 MB uncompressed, 200 files, 10 MB per file. The slide lands in draft.

  • Auth: scope decks:write

Input

FieldTypeDescription
filenamestring (required)Must end in .zip.
contentBase64string (required)Base64-encoded zip. Decoded size must be ≤ 50 MB.
titlestringDeck title override; defaults to the zip filename.

Response

{
  "id": 6,
  "title": "Product Demo",
  "slug": "product-demo-s1u5q",
  "status": "draft",
  "bundleFileCount": 12,
  "bundlePrefix": "media/abc123/",
  "url": "https://cdn.simplerdevelopment.com/media/abc123/index.html",
  "approval": { "url": "https://app.simplerdevelopment.com/approve/stu901" }
}

Errors: { "error": "Invalid base64 content" }, { "error": "Zip exceeds <max> bytes" }, { "error": "Empty zip" }


decks_delete#

Permanently delete a deck and all its versions.

  • Auth: scope decks:write

Input

FieldTypeDescription
idnumber (required)Deck ID.

Response: { "success": true, "id": 4 } or a pending approval object.

Errors: { "error": "Deck not found" }


deck_analytics_get#

Return viewer analytics for a single pitch deck: total view events, unique viewer sessions, and per-slide view counts with average dwell time. The deck must belong to the authenticated client.

  • Auth: scope decks:read

Input fields:

FieldTypeRequiredDescription
deckIdnumberYesID of the pitch deck to analyse.

Response:

{
  "deckId": 4,
  "title": "Investor Deck 2026",
  "totalEvents": 312,
  "uniqueSessions": 47,
  "perSlide": [
    { "slideIndex": 0, "views": 47, "avgDwellMs": 8200 },
    { "slideIndex": 1, "views": 43, "avgDwellMs": 14500 },
    { "slideIndex": 2, "views": 38, "avgDwellMs": null }
  ]
}

avgDwellMs is null when no dwell-time data has been recorded for that slide yet. totalEvents counts all view events (a single session viewing the same slide twice counts twice); uniqueSessions counts distinct session IDs.

Errors:

ConditionResponse
Deck not found or wrong tenant{ "error": "Deck not found" }

Automations#

automations_list#

List all automation rules for your account, including trigger, conditions, and actions.

  • Auth: scope automations:read

Input

FieldTypeDescription
enabledbooleanFilter to only enabled or disabled rules.
productScopestringFilter by product scope (e.g. "email", "crm").

Response

[
  {
    "id": 10,
    "name": "Tag CRM contact on survey submit",
    "description": null,
    "trigger": { "event": "survey.response.created", "surveyId": 7 },
    "conditions": [],
    "actions": [{ "tool": "crm_contacts_update", "params": { "tags": ["intake-complete"] } }],
    "enabled": true,
    "productScope": "crm",
    "source": "manual",
    "updatedAt": "2026-03-01T00:00:00.000Z"
  }
]

automations_create#

Define a new automation rule. Rules start enabled.

  • Auth: scope automations:write

Input

FieldTypeDescription
namestring (required)Rule name.
descriptionstringOptional description.
triggerobject (required){ event: string, ...metadata } — e.g. { "event": "email.campaign.sent", "campaignId": 88 }.
conditionsarrayFilter conditions: [{ field, operator, value }]. Empty = always run.
actionsarray (required)Action list: [{ tool: string, params: object }].
enabledbooleanDefaults to true.
productScopestringGrouping label (e.g. "email", "crm").
sourcenlp | settings | manualDefaults to "manual".

Request body

{
  "name": "Subscribe on booking",
  "trigger": { "event": "booking.created" },
  "conditions": [],
  "actions": [
    { "tool": "email_subscribers_add", "params": { "listId": 12 } }
  ]
}

Response: Full automation rule row.


automations_update#

Update name, description, trigger, conditions, actions, or productScope on an existing rule. Use automations_toggle to change only the enabled flag.

  • Auth: scope automations:write

Input

FieldTypeDescription
idnumber (required)Rule ID.
namestringNew name.
descriptionstring | nullNew description.
triggerobjectReplacement trigger blob.
conditionsarrayReplacement conditions array.
actionsarrayReplacement actions array.
productScopestring | nullNew product scope.

Response: Updated automation rule row.

Errors: { "error": "Rule not found" }


automations_toggle#

Flip the enabled flag on a rule without touching trigger, conditions, or actions.

  • Auth: scope automations:write

Input

FieldTypeDescription
idnumber (required)Rule ID.
enabledboolean (required)true to enable, false to disable.

Response: Updated automation rule row.

Errors: { "error": "Rule not found" }


automations_delete#

Permanently delete an automation rule. Execution logs are retained.

  • Auth: scope automations:write

Input

FieldTypeDescription
idnumber (required)Rule ID.

Response

{ "success": true, "id": 10 }

Errors: { "error": "Rule not found" }