Topolo Docs Topolo Platform
Open workspace

Nurture

Worked guidance for 131 public Nurture actions.

Nurture action reference

Discover the exact action and authorized resource before execution.

  • Get widget summary: Get the TopoloOne widget summary for this service.
  • Get workspace bootstrap: Get the verified user and organization context for this service.
  • List subscription topics: List the subscription topics in the caller's Nurture workspace, optionally filtered by status. Topics are what a contact can subscribe to in the preference centre; the contact's choice itself is held by TopoloConsent.
  • Get subscription topic: Read one subscription topic by id from the caller's Nurture workspace.
  • Create subscription topic: Create a subscription topic in the caller's Nurture workspace. The key is unique per workspace and doubleOptIn defaults to true.
  • Update subscription topic: Change a subscription topic's fields. Only the fields sent change.
  • Archive subscription topic: Archive a subscription topic so it can no longer be used for new work. Its history is kept.
  • List segments: List the segments in the caller's Nurture workspace, optionally filtered by status. A segment is a rule set over contact fields (Nurture contacts, or the linked CRM workspace), Nurture lifecycle events and the lead score (kind lead_score: operator qualified, or at_least with a value).
  • Get segment: Read one segment by id from the caller's Nurture workspace.
  • Create segment: Create a segment in the caller's Nurture workspace. Rules match all or any of up to 20 conditions on contact fields or event recency.
  • Update segment: Change a segment's fields. Only the fields sent change.
  • Archive segment: Archive a segment so it can no longer be used for new work. Its history is kept.
  • Recompute segment membership: Re-evaluate one segment and record who entered and who exited. It always evaluates the workspace's own active Nurture contacts (fields email, first_name, last_name, locale, timezone, source and every attribute key), including contacts a connector synced in with connectors.sync; it never reads a remote system. Recent lifecycle events are joined by contact id.
  • List segment members: List the contacts currently in a segment as of its last recompute, newest entries first.
  • List contacts: List the contacts Nurture holds for the caller's workspace, ordered by email, optionally filtered by status and a prefix search on email, first or last name. Pages of up to 200; pass nextCursor back as cursor for the next page.
  • Export contacts: Export the workspace's contacts as JSON for data portability, optionally filtered by status, ordered by email. Returns at most 5000 contacts; truncated is true when more exist, in which case page through contacts.list instead.
  • Import contacts from CSV: Import up to 10,000 contacts from CSV text (RFC 4180, at most 2 MB, header row required), parsed and planned by the shared @topolo-io/audience importer. mapping names the column for email and, optionally, firstName, lastName, locale, timezone and attribute keys; omit it to use suggestedMapping (returned on every call, derived from the header names). mode preview writes nothing and reports what would happen; mode commit creates new contacts (source csv) and updates names, locale, timezone and attributes (merged) of existing active contacts, matched case-insensitively by email. Archived contacts are skipped, never unarchived; rows identical to the stored contact count as unchanged; repeated emails in the file keep the first row. invalid[].line is the CSV record number (header = 1).
  • Get contact: Read one Nurture contact by id, including its attributes and source.
  • Create contact: Add one contact to the workspace audience with source manual. The email is stored as entered (trimmed) and must be unique per workspace, case-insensitively; attributes is a flat map of up to 50 string, number or boolean values that segment rules can match by key.
  • Update contact: Change an active contact's email, names, locale, timezone or attributes. Only the fields sent change; attributes, when sent, replace the whole map; null clears a name, locale or timezone.
  • Archive contact: Archive a contact so segments stop matching it and imports no longer update it. The record is kept; use erase for a right-to-erasure request.
  • Erase contact permanently: Right to erasure: permanently delete one contact, remove all of its segment membership history, journey enrollments (with their steps and intended sends), lead score, score history and lead handoff records, subscription history, confirmation tokens, send reservations and contact-scoped suppression events, and detach it from every lifecycle event (the events remain without a contact). Org-wide address suppressions (hard_bounce, deliverable, complaint, wrong_person) are deliberately KEPT, keyed only by the salted address hash, so an erased address that bounced or complained can never be mailed again, and an address-scoped erased suppression event is recorded so the erased address stays suppressed org-wide for every purpose, even if it is imported again (nothing lifts it). Requires confirm: true. Event properties sent by callers are not rewritten.
  • Get the lifecycle event catalog: List every typed lifecycle event Nurture records (signup, trial_started, trial_ending, form_submitted, purchase, subscription_changed, feature_used, the derived inactive and mql, engage_not_now) with its property JSON Schema, whether it can be ingested, and how its contact resolves (contactId, then email, then externalId).
  • Ingest lifecycle events: Record one typed lifecycle event, or a batch of up to 100 as {events: [...]}, from app usage or any system. Each event is validated against events.catalog, names its contact by contactId, email or externalId (email is used to resolve, never stored; no contact or subscription is created), and is stored once per idempotency key. New events run journey triggers and goals, then rescore the contact against the published lead scoring model. Derived types (inactive, mql) are reserved: 422 reserved_event_type.
  • List lifecycle events: List the newest lifecycle events in the workspace, optionally filtered by event type or contact, up to 200.
  • Get the inactivity policy: Get the workspace inactivity window that drives the derived inactive lifecycle event. Disabled at 30 days until set.
  • Set the inactivity policy: Enable or disable the derived inactive event and set its window (1-365 days). When enabled, the Nurture scheduler records one inactive event per contact whose newest lifecycle event is older than the window, once per stretch of inactivity, and runs journey triggers for it.
  • List broadcasts: List the broadcasts in the caller's Nurture workspace, newest change first, optionally filtered by status (draft, pending_approval, approved, scheduled, sending, sent, cancelled, failed, archived).
  • Get broadcast: Read one broadcast with its template pin, audience, schedule, throttle, approval trail (who submitted, approved, rejected, scheduled or cancelled it, and when) and its 20 most recent test sends.
  • Create broadcast: Create a draft broadcast: a template (optionally a pinned templateVersion), a subscription topic, one to 20 segments, a schedule and an optional throttlePerHour. Purpose is always marketing. Nothing is sent; the draft must be submitted, approved by a different person, and scheduled. schedule is {mode:"now"} (default), {mode:"at", sendAt ISO-8601} or {mode:"recipient_local", date YYYY-MM-DD, time HH:MM, fallbackTimezone IANA}: recipient_local sends at that wall-clock time in each contact's own zone, using fallbackTimezone when the contact has none.
  • Update broadcast draft: Change a draft broadcast's name, template, pinned templateVersion (null = current version at submit), topic, segments, schedule or throttlePerHour (null = unthrottled). Only drafts can change; reject a pending or approved broadcast back to draft first. Changing templateId clears the pinned version unless one is given. schedule is {mode:"now"} (default), {mode:"at", sendAt ISO-8601} or {mode:"recipient_local", date YYYY-MM-DD, time HH:MM, fallbackTimezone IANA}: recipient_local sends at that wall-clock time in each contact's own zone, using fallbackTimezone when the contact has none.
  • Archive broadcast: Archive a broadcast that is a draft or has finished (sent, cancelled or failed) so it leaves the working list. Its recipients and history are kept. A broadcast awaiting approval, approved, scheduled or sending must be cancelled first.
  • Submit broadcast for approval: Submit a draft for approval. Checks that the topic and every segment are active, the schedule is still ahead, and the template renders without errors; pins the template version (the given one, else the current one) so the approver reviews exactly what will be sent. The submitter is recorded and cannot approve it.
  • Approve broadcast: Approve a broadcast awaiting approval. The approver must be a different verified user from the submitter (both ids come from the caller's credential); the same user is refused with approval_requires_second_person. Approval does not send anything; schedule it next.
  • Reject broadcast: Return a broadcast that is awaiting approval, or already approved but not yet scheduled, to draft with a reason. The approval and submission are cleared, so it must be submitted and approved again.
  • Schedule broadcast: Queue an approved broadcast. The scheduler (every 5 minutes) launches it at its launch time: now, sendAt, or for recipient_local the moment the local time first occurs anywhere (UTC+14). Launch snapshots the active contacts in its segments; recompute segments beforehand for fresh membership. Recipients are then sent in throttled batches. Each recipient goes through eligibility.reserve (subscription to the topic, suppressions, caps, quiet hours) with key broadcast:{id}:{contactId}; a granted reservation is consumed once and recorded as sent_intended. With a verified sender (sending_settings.get) the send goes out through Nexus email.send, or waits in the outbox when no Nexus credential is usable; with no sender configured it is only recorded.
  • Cancel broadcast: Cancel a broadcast that is awaiting approval, approved, scheduled or sending. Every recipient not yet processed is marked cancelled and never sent; recipients already processed keep their outcome (their consumed reservations still count toward caps). Cancelling is final.
  • Test send broadcast: Render the broadcast's template (pinned version, else current) for up to 5 named addresses, merged with sampleContactId when given, and record a test send labelled "[Test]" in the subject. It bypasses the audience and eligibility and is allowed in any status but archived. With a verified sender (sending_settings) each address gets one email through Nexus email.send as the caller (transactional class, idempotent per address); deliveries[] reports sent with the Nexus message id, or not_sent with a reason. The rendered html and text are returned for review.
  • List broadcast recipients: Page through a launched broadcast's audience snapshot by contact id: each recipient's zone, due time, status (pending, sending, sent_intended, skipped, failed, cancelled), attempts, reservation id and skip or failure reasons. Pass nextCursor as cursor for the next page.
  • Get broadcast progress: Read delivery progress: the broadcast status, total recipients, counts per recipient status, processed (sent_intended + skipped + failed), progress 0-1, and how many recipients were skipped for each reason.
  • List journeys: List the journeys in the caller's Nurture workspace, newest change first, optionally filtered by status (draft = never published, published = has a live version, archived).
  • Get journey: Read one journey: its editable draft definition, status, live currentVersion and whether the draft has unpublished changes.
  • Create journey draft: Create a journey as an unpublished draft. definition = { trigger { eventTypes[], segmentIds[] }, entryNodeId, nodes[], allowReentry? }. Nodes: delay { delay { mode duration, minutes } | { mode time_of_day, time HH:MM, fallbackTimezone }, next }, branch { condition contact_field | event (withinDays) | segment (member/not_member), yes, no }, send { templateRef, purpose, topicKey (required unless transactional), messageCategory (transactional), next }, split { arms [{ key, weight 0-100, next }] (2-5), holdoutPercent 0-50 } (A/B test: hash of experiment and contact picks the arm; holdout exits), goal { eventType } (journey-wide: a goal event exits the contact as converted), exit. The draft is saved even when the graph is incomplete; problems[] lists what publish would reject. Nothing is enrolled or sent.
  • Update journey draft: Change a journey's name, description or draft definition. Editing a published journey creates a new draft (hasUnpublishedChanges true); the live version and every in-flight enrollment keep running unchanged until app_topolo_nurture.journeys.publish and an explicit app_topolo_nurture.journeys.enrollments.migrate. Returns problems[] for the saved draft.
  • Archive journey: Archive a journey: it stops enrolling contacts, every active enrollment exits with reason journey_archived, and the draft can no longer change. Versions, enrollment history and intended sends are kept.
  • Validate journey draft: Check the draft exactly as publish would: one entry node, every edge pointing at an existing non-goal node, every node reachable, no loop without a delay, at least one exit, send nodes with a topicKey (or messageCategory when transactional), known fallback time zones, and active referenced segments and topics.
  • Publish journey version: Freeze the current draft as the next immutable version and make it live: new trigger matches and manual enrollments use it from now on. Existing enrollments stay pinned to the version they entered on. Refused when the draft is invalid or unchanged since the last publish.
  • List journey versions: List the immutable published versions of a journey, newest first, with each version's full definition.
  • Get journey version: Read one immutable published version of a journey by number.
  • Simulate journey: Walk the draft (or a published version) for one real contact (contactId) or an inline sample { fields, timezone, events[], segmentIds[] } on a simulated clock from startAt (default now), jumping over delays, and return the path with timings. Side-effect free: nothing is enrolled, reserved or recorded; for a real contact each send asks eligibility.check what would happen at that simulated time (intended, deferred, blocked). Stops at exit, goal conversion or maxSteps (truncated true).
  • List journey enrollments: List a journey's enrollments, newest first, optionally by status (active, completed, converted, exited); up to 200. Each enrollment is pinned to a version and shows the node it will run next and when (dueAt).
  • Get journey enrollment: Read one enrollment with its step history (each node taken, outcome and time) and the sends it intended: each send node took an eligibility reservation (status intended) or records why it could not (blocked, deferred, failed). Each intended send is delivered through Nexus email.send when the workspace has a verified sender (sending_settings.get), queued in the outbox when no Nexus credential is usable, and only recorded when no sender is configured.
  • Enroll contact in journey: Put one active contact on a published journey's live version, due immediately: the scheduler (every 5 minutes) then advances it, and each send node takes an eligibility reservation, so suppression, subscriptions, caps and quiet hours apply. Idempotent: a contact already active on the journey returns that enrollment with created false. Refused when the journey is not published, or the contact already went through it and the version has allowReentry off.
  • Exit journey enrollment: End one active enrollment now with status exited and the given reason; it takes no further steps. Already-ended enrollments are refused.
  • Migrate enrollments to live version: Explicitly move active enrollments from an older version (fromVersion) to the journey's live version, up to 500 per call (more true means call again). Safe rule: an enrollment moves only when the node it stands on exists in the live version with the same id and kind; it then continues from that node. Others stay pinned and are listed in skipped[] with a reason. dryRun true reports without changing anything.
  • List journey blueprints: List the prebuilt journey blueprints (welcome_series, trial_onboarding_14d, re_engagement, win_back, not_now_90d): each one's trigger (lifecycle events, or segment entry for win_back), goal event, send steps and delay steps with default timings. Blueprints are structure only and contain no message copy.
  • Get journey blueprint: Read one journey blueprint with its steps, the placeholder starterTemplate every created step template starts from, and a preview journey definition built with default timings (send templateRefs are starter:<step> placeholders and topicKey is topic-key).
  • Create journey from blueprint: Create a DRAFT journey from a blueprint. Pass requestKey (a retry with the same key returns the same draft and templates, replayed true), topicKey (an active subscription topic), optional name, purpose (marketing or outreach), segmentId (required for win_back, where entering it enrolls; otherwise an audience filter), delays {delayStepId: minutes} and templateIds {sendStepId: templateId}. Send steps without a template id need createStarterTemplates true: each gets a new template holding only bracketed placeholders ([Subject], [Heading], [Write your message here]) for a person to replace. Nothing is published, enrolled or sent; publish with app_topolo_nurture.journeys.publish after editing.
  • Get workspace settings: Read this Nurture workspace's settings. The CRM link formerly stored here (crmWorkspaceId) is now a TopoloCRM connector: a workspace that still had one gets an equivalent connector (capability contact_source) on this call and the setting is cleared.
  • Update workspace settings: Save this Nurture workspace's settings (no editable fields remain: the CRM link moved to connectors.connect). Migrates a legacy crmWorkspaceId link into a TopoloCRM connector once, like settings.get.
  • List Notify source adapters: List the fixed, versioned adapters from TopoloNotify event types (published @topolo-io/notifications taxonomy) to Nurture lifecycle event types for Auth, Forms, Pay, Commerce and Engage, plus Nexus nexus.email.* delivery events (lifecycleEventType nexus_email_delivery: applied to the matching send as delivery engagement and suppressions, not ingested as lifecycle events). Available entries can be connected; unavailable entries name the missing published contract and the events.ingest fallback.
  • List event sources: List the Notify event sources connected (or previously connected) in the caller's Nurture workspace, newest first.
  • Connect a Notify event source: Subscribe this workspace to one TopoloNotify event type from one app, so each matching platform event is recorded as a Nurture lifecycle event. For Nexus delivery events connect each nexus.email.* type from app_topolo_nexus: only events for Nurture's own sends (app_id and entity_ref_id) in this workspace are applied, and with all seven connected delivery polling stops. Creates a Notify webhook subscription as the caller and sets its signing secret to one Nurture derives; no secret is stored.
  • Disconnect an event source: Delete the source's TopoloNotify webhook subscription (app_topolo_notify.webhooks.subscriptions.delete) as the caller and mark the source disconnected. Events already recorded are kept. Repeating it on a disconnected source is harmless.
  • List contact subscriptions: Read a contact's subscription state for every active topic of the workspace: none, pending_confirmation, subscribed or unsubscribed, with since and the evidence that subscribed them. State is resolved by @topolo-io/audience from Nurture's own subscription stream and suppression ledger; an unsubscribe is always a ledger event.
  • Subscribe contact with evidence: Subscribe a contact to an active topic on the strength of evidence. consent_decision (provider, decisionRef, purposeId) always subscribes, also after an unsubscribe. import_attestation (importId, lawfulBasis; attestedBy is the caller) is accepted only as the contact's very first subscription to the topic, never after an unsubscribe, and never for a topic with doubleOptIn true. For double opt-in use subscriptions.request_confirmation instead; double_opt_in evidence is only ever produced by the public confirmation link. Marketing delivered through Nexus is attested to Nexus from the latest double opt-in, repermission or consent_decision record; an import_attestation alone is not consent and the send skips with no_consent_evidence.
  • Request double opt-in confirmation: Start double opt-in for a contact and an active topic: records subscribe_requested (state pending_confirmation) and returns confirmationUrl, a signed single-use link valid for 7 days (only its hash is stored). Opening the link (GET) only shows a page with a Confirm subscription button and never changes state, so email link scanners cannot confirm; the button POSTs the token, which confirms the subscription with double_opt_in evidence. With a verified sender (sending_settings) Nurture emails the link through Nexus (transactional, service_notice) and reports it in email; without one email.status is not_configured: deliver confirmationUrl yourself. Never confirm on the contact's behalf.
  • Issue preference-centre link: Issue a signed preference-centre link for one active contact, valid for 365 days. The page (GET never changes state) lists the workspace's active topics with the contact's state; its buttons POST the token to subscribe to a topic (always double opt-in), unsubscribe from a topic, or unsubscribe from everything (a topic-less unsubscribed ledger event, the global opt-out). The token is a topic-less unsubscribe token, so the link also works with the one-click unsubscribe endpoint. Nothing is stored and links cannot be revoked individually; deliver it only to the contact's own address.
  • Unsubscribe contact from topic: Unsubscribe a contact from one topic (active or archived) by appending a topic-scoped unsubscribed event to the suppression ledger, the single source of unsubscribes. Marketing for that topic is blocked until the contact re-subscribes with double opt-in or a consent decision.
  • List contact suppressions: Read the suppression blocks currently active for a contact (resolved by @topolo-io/audience, stricter wins) and the ledger events behind them. Address-scoped events (hard_bounce, deliverable, complaint, wrong_person, and erased, which contacts.erase records and nothing lifts) are keyed by the org-scoped address hash and apply across every workspace of the organization, so events may come from other workspaces; events are therefore shown without contact ids, address hashes or evidence details.
  • Record suppression event: Append one event to the contact's suppression ledger. Blocking events: unsubscribed (all marketing, or one topic with topicKey), hard_bounce, complaint, wrong_person (these three apply org-wide to the address), reply_stop, not_interested (outreach), manual_block (everything). Loosening events need evidence: deliverable (clears hard_bounce) needs verification; repermissioned (clears unsubscribed, reply_stop, not_interested) needs consent_decision, and clearing a complaint also needs verification; manual_unblock (clears manual_block) is an operator action. import_attestation never lifts a block. double_opt_in evidence cannot be sent here; it is produced only by the public confirmation link.
  • Check send eligibility: Decide, without reserving anything, whether a message may be sent now to up to 100 contacts. Rules come from @topolo-io/audience over Nurture's own data: suppression blocks (hard bounce, complaint, wrong person, erased address, manual block, unsubscribes), topic subscription state when purpose is marketing with a topicKey, and the workspace pacing policy (rolling 24h and 7d caps counting reserved and consumed sends, quiet hours in the recipient's timezone). Transactional messages need messageCategory and skip subscription, caps and quiet hours but still honour hard_bounce, wrong_person, erased and manual_block. A missing or archived contact is ineligible with no_contact_point. nextEligibleAt is present only when every reason clears with time.
  • Reserve a send slot: Check eligibility for one contact and, when eligible, atomically reserve a send slot that counts toward the caps for 15 minutes (or until released). Idempotent by idempotencyKey: a repeat returns the same live reservation without re-counting caps, or granted false with reservation_expired once released or expired. Applies the same suppression, subscription and pacing rules as app_topolo_nurture.eligibility.check; transactional messages need messageCategory. For marketing and outreach a granted reservation includes listUnsubscribe: a signed one-click unsubscribe URL and POST body for the message body. Nexus owns the List-Unsubscribe headers; pass the preference centre to email.send as unsubscribe_landing_url. With an active suppression_sync connector, a connector-sourced contact is also reserved in that system (TopoloCRM eligibility.reserve) and the stricter answer wins: a remote refusal releases the Nurture reservation and returns its reasons. Sends nothing.
  • Release a send reservation: Release an unused reservation so it stops counting toward the contact's caps. Releasing a reservation that is already released or consumed changes nothing. A remote reservation taken through a connector is released there too (remote[]).
  • Get pacing policy: Read the workspace's send pacing policy. Without a saved policy Nurture uses maxPerDay 1, maxPerWeek 3, no quiet hours, crossAppSameDay allow and consentRequiredForMarketing false; updatedAt is null until a policy is saved.
  • Update pacing policy: Change the workspace's send pacing policy; fields not sent keep their value. maxPerDay and maxPerWeek are rolling 24h and 7d caps (null = uncapped). quietHours { start, end as HH:MM, timezone "recipient" or an IANA zone, fallbackTimezone } blocks marketing sends inside the window (null = none). crossAppSameDay block only has an effect once a connector shares other apps' sends; Nurture alone sends from one app. consentRequiredForMarketing true blocks marketing until a consent connector exists.
  • List connector providers: List the remote audience systems a Nurture workspace can optionally connect, the capabilities each supports and the non-secret config fields it needs. Nurture works fully without any connector. Capabilities: contact_source (connectors.sync copies remote contacts into Nurture, source connector, source_ref crm:<id>); suppression_sync (connectors.sync pushes unsubscribed, hard_bounce, complaint and manual_block events for those contacts to TopoloCRM, and eligibility.reserve also reserves in TopoloCRM, stricter wins); interaction_writeback (records sends on the CRM timeline; nothing writes yet); lead_handoff (TopoloCRM: a task per newly qualified CRM-linked lead; TopoloEngage: enrol each newly qualified lead into the chosen campaign from the chosen seat, creating the Engage contact when missing).
  • List connectors: List the connectors in the caller's Nurture workspace (any status) with their enabled capabilities, config, last sync time, resume cursor and last error. A workspace that still has the retired settings.crmWorkspaceId link gets an equivalent TopoloCRM connector (contact_source) on this call.
  • Connect a provider: Connect one provider to this workspace with the chosen capabilities and its non-secret config (topolo_crm: crmWorkspaceId; topolo_engage: engageWorkspaceId, plus optional campaignId and seatId, checked in Engage). Calls act as the caller's credential; no secret is stored. One connector per provider per workspace; reconnecting a disconnected one reactivates it with the new settings. Capabilities: contact_source (connectors.sync copies remote contacts into Nurture, source connector, source_ref crm:<id>); suppression_sync (connectors.sync pushes unsubscribed, hard_bounce, complaint and manual_block events for those contacts to TopoloCRM, and eligibility.reserve also reserves in TopoloCRM, stricter wins); interaction_writeback (records sends on the CRM timeline; nothing writes yet); lead_handoff (TopoloCRM: a task per newly qualified CRM-linked lead; TopoloEngage: enrol each newly qualified lead into the chosen campaign from the chosen seat, creating the Engage contact when missing).
  • Update a connector: Change a connector's capabilities, config, or status (active or paused). A paused connector keeps its settings but neither syncs nor takes part in eligibility or lead handoff. Only the fields sent change; config is replaced whole, so send every field. A topolo_engage campaignId and seatId are checked in Engage as the caller. Use connectors.disconnect to disconnect.
  • Disconnect a connector: Stop using a connector: it no longer syncs or delegates eligibility. Contacts it synced stay in Nurture with their source, and nothing in the remote system changes. Repeating it is harmless.
  • List Engage handoff targets: For a topolo_engage connector, list the TopoloEngage campaigns (not archived or completed) and seats in its Engage workspace, read as the caller through app_topolo_engage.campaigns.list and app_topolo_engage.seats.list. Pick one campaign id and one seat id and save them with connectors.update config campaignId and seatId; qualified leads are then enrolled there. Until both are set, Engage handoffs stay pending.
  • Sync a connector: Run one bounded sync as the caller: with contact_source, page the remote contacts (up to 50 pages, resuming from lastSyncCursor) and upsert them into Nurture contacts; with suppression_sync, push pending blocking ledger events for connector-sourced contacts. A sync only writes contact profiles: it never changes an unsubscribe or suppression, never unarchives a contact, and never takes over an address Nurture already holds from another source (counted as skipped). Safe to repeat.
  • List signup forms: List the signup forms in the caller's Nurture workspace, optionally filtered by status. Each form is a public hosted page (hostedUrl, embeddable with embedHtml; null until NURTURE_PUBLIC_URL is configured) for one subscription topic. A submission creates or reuses the contact (source signup) and always starts double opt-in; it never subscribes directly. The confirmation email goes out through Nexus email.send from the workspace's verified sender (sending_settings.get); it waits in the outbox when no Nexus credential is usable.
  • Get signup form: Read one signup form by id from the caller's Nurture workspace.
  • Create signup form: Create a signup form in the caller's Nurture workspace. The topic must be active. collectName (default false) adds optional first and last name fields. The page answers every valid submission identically, has a honeypot field and starts at most one confirmation per contact and topic every 10 minutes.
  • Update signup form: Change a signup form's fields. Only the fields sent change.
  • Archive signup form: Archive a signup form so it can no longer be used for new work. Its history is kept.
  • List email templates: List the email templates in the caller's Nurture workspace with their current subject and version, optionally filtered by status.
  • Get email template: Read one email template with its subject, preheader and blocks. Without version it returns the current version; with version it returns that immutable historical version.
  • Create email template: Create an email template (version 1) in the caller's Nurture workspace. Nothing is sent. blocks is an ordered list of typed blocks: heading {text, level 1-3}, text {text with bold, italic and label only}, image {src https, alt, width?, href?}, button {label, href https or mailto}, divider, spacer {height}, columns {2-3 columns of the other leaf blocks}, and exactly one footer {text?} as the LAST block, which always renders the unsubscribe and preference links plus the brand footer and postal address. Raw HTML is never accepted; every value is escaped. Merge tags {{contact.firstName|fallback}}, {{contact.lastName}} and {{contact.email}} are allowed in the subject, preheader and text.
  • Update email template: Change a template. Renaming changes it in place; any subject, preheader or blocks change appends a new immutable version and makes it current, so earlier versions stay readable. blocks is an ordered list of typed blocks: heading {text, level 1-3}, text {text with bold, italic and label only}, image {src https, alt, width?, href?}, button {label, href https or mailto}, divider, spacer {height}, columns {2-3 columns of the other leaf blocks}, and exactly one footer {text?} as the LAST block, which always renders the unsubscribe and preference links plus the brand footer and postal address. Raw HTML is never accepted; every value is escaped. Merge tags {{contact.firstName|fallback}}, {{contact.lastName}} and {{contact.email}} are allowed in the subject, preheader and text.
  • Archive email template: Archive an email template so it can no longer be edited or chosen for new email. Its versions are kept for history.
  • Duplicate email template: Copy a template's current version into a new active template (version 1). Works on archived templates too. The name defaults to "<name> (copy)".
  • Preview email template: Render a saved template version (templateId, version?) or an unsaved draft to email-safe HTML (tables, inline styles, dark-mode and mobile rules) plus the auto-generated plain-text alternative, for an optional sample contact. Brand name, email logo, light and dark accent colours, footer, legal entity, postal address and unsubscribe wording come from the workspace TopoloBrand kit when one is bound and readable, else the workspace template brand settings, else neutral defaults; Brand being unavailable never fails a preview. Unsubscribe and preference links are inert placeholders (#unsubscribe, #preferences); a real send substitutes signed recipient links. No side effects: nothing is saved or sent.
  • Validate email template: Check a saved template version or an unsaved draft for problems without saving: a missing or misplaced unsubscribe footer, duplicate block ids, unknown merge fields and unsafe inline links are errors; missing image alt text and a missing brand postal address are warnings.
  • Get template brand settings: Read the workspace's template brand settings (brand name, logo, primary colour, footer text, postal address) and the brand emails actually render with, including which source supplied each value (brand, settings or default). A bound TopoloBrand kit wins; when Brand is unbound or unavailable the settings apply. Unset values render neutral; nothing is invented.
  • Update template brand settings: Set the workspace fallback brand values email templates render with when no Brand kit supplies them: brandName, logoUrl (https), primaryColor (#RRGGBB), footerText and postalAddress. Send null to clear a value. Only the fields sent change.
  • Get lead scoring model: Read the workspace's lead scoring model: the editable draft, the published version scores are computed against, whether the draft has unpublished changes, and the handoff targets (active lead_handoff connectors, plus targets Nurture cannot reach yet and why). Score = fit + behaviour. Fit rules add points when a contact field matches (equals, not_equals, contains, exists, not_exists). Behaviour rules add points per matching lifecycle event (optionally only when properties[key] equals a value), each event halving every halfLifeDays (0 = no decay), with the rule clipped to maxPoints. A contact qualifies when score >= mqlThreshold.
  • Update lead scoring draft: Replace the draft lead scoring model (behaviourRules, fitRules, mqlThreshold; up to 50 rules each with unique ids). Scores do not change until scoring_model.publish. Score = fit + behaviour. Fit rules add points when a contact field matches (equals, not_equals, contains, exists, not_exists). Behaviour rules add points per matching lifecycle event (optionally only when properties[key] equals a value), each event halving every halfLifeDays (0 = no decay), with the rule clipped to maxPoints. A contact qualifies when score >= mqlThreshold.
  • Publish lead scoring model: Freeze the draft as the next model version, make it current and rescore every active contact (up to 5000; truncated says when more remain, finish with scoring.recompute). The first time a contact qualifies Nurture records one mql lifecycle event (journey trigger and goal, segment event condition) and queues a handoff to every active connector with lead_handoff (TopoloCRM: a follow-up task on the linked CRM contact; TopoloEngage: enrolment into the connector's Engage campaign from its seat, skipped for an archived Engage contact or an address with a hard bounce, complaint or wrong-person block); without one the contact stays in Nurture (scoring.qualified.list, segment condition lead_score).
  • Recompute lead scores: Rescore one active contact (contactId) or every active contact (up to 5000) against the published model at the current time, then deliver pending lead handoffs this caller can reach (handoffs queued by the scheduled decay pass or a Notify delivery wait for a credentialed call like this one). Safe to repeat: an unchanged score writes no history and each contact qualifies once. The first time a contact qualifies Nurture records one mql lifecycle event (journey trigger and goal, segment event condition) and queues a handoff to every active connector with lead_handoff (TopoloCRM: a follow-up task on the linked CRM contact; TopoloEngage: enrolment into the connector's Engage campaign from its seat, skipped for an archived Engage contact or an address with a hard bounce, complaint or wrong-person block); without one the contact stays in Nurture (scoring.qualified.list, segment condition lead_score).
  • List qualified leads: List active contacts whose current lead score is at or above the MQL threshold, highest first (up to 500), with email, score breakdown, when they first qualified and each lead handoff's status. This is where qualified leads stay when no lead_handoff connector is enabled; the segment condition lead_score (operator qualified) holds the same set.
  • Get contact lead score: Read one contact's current lead score (fit, behaviour, total, qualified, per-rule reasons, model version, when it first qualified) with the current MQL threshold and its lead handoffs. score is null until the contact has been scored against a published model.
  • List contact score history: List one contact's lead score changes, newest first (up to 200): each entry has fit, behaviour, total, the change (delta), whether it qualified, the model version, the cause (event:<type>, model:v<n>, recompute or decay) and the rule contributions behind it.
  • Get workspace report: Report the whole Nurture workspace: headline conversions and revenue, clicks and delivery engagement, opens (with a privacy caveat), and breakdowns by journey/broadcast (bySource) and template (byTemplate). Covers messages sent in the last days (default 30, max 365). Conversions are goal lifecycle events (attribution_settings, default purchase) credited last-touch: the latest send (or click, basis click) to that contact within windowDays before the event gets it and its revenue (properties amountMinor + currency, summed per currency). Engagement is unique per send. Opens only count with open tracking on and are approximate (opens.caveat); judge on conversions and revenue.
  • Get journey report: Report one journey's sends (every version and send node): headline conversions and revenue, clicks, delivery engagement, opens with a privacy caveat, and a per-template breakdown. Covers messages sent in the last days (default 30, max 365). Conversions are goal lifecycle events (attribution_settings, default purchase) credited last-touch: the latest send (or click, basis click) to that contact within windowDays before the event gets it and its revenue (properties amountMinor + currency, summed per currency). Engagement is unique per send. Opens only count with open tracking on and are approximate (opens.caveat); judge on conversions and revenue.
  • Get broadcast report: Report one broadcast's recipient sends: headline conversions and revenue, clicks, delivery engagement and opens with a privacy caveat. Delivery progress (pending/skipped recipients) is broadcasts.stats. Covers messages sent in the last days (default 30, max 365). Conversions are goal lifecycle events (attribution_settings, default purchase) credited last-touch: the latest send (or click, basis click) to that contact within windowDays before the event gets it and its revenue (properties amountMinor + currency, summed per currency). Engagement is unique per send. Opens only count with open tracking on and are approximate (opens.caveat); judge on conversions and revenue.
  • Get template report: Report every send rendered from one template id, across journeys and broadcasts: headline conversions and revenue, clicks, delivery engagement and opens with a privacy caveat. An id nothing was sent from reports zeros. Covers messages sent in the last days (default 30, max 365). Conversions are goal lifecycle events (attribution_settings, default purchase) credited last-touch: the latest send (or click, basis click) to that contact within windowDays before the event gets it and its revenue (properties amountMinor + currency, summed per currency). Engagement is unique per send. Opens only count with open tracking on and are approximate (opens.caveat); judge on conversions and revenue.
  • Get attribution settings: Read how Nurture credits conversions: goalEventTypes (lifecycle event types that count as a conversion, default ["purchase"]), windowDays (default 7) and basis (send: the window opens at each send; click: only clicked sends compete). The model is always last_touch.
  • Update attribution settings: Change how conversions are credited: goalEventTypes (1-10 lifecycle event types, e.g. purchase, subscription_changed, mql), windowDays (1-90) and basis (send or click). Only the fields sent change. Reports are computed on read, so every report, past periods included, uses the new settings immediately; nothing stored is rewritten.
  • Get tracking settings: Read whether Nurture tracks clicks (on by default: external https links become signed redirects that only lead to the URL recorded when the message was rendered) and opens (off by default: a 1x1 pixel; approximate and privacy-sensitive, see opensCaveat).
  • Update tracking settings: Turn click tracking or open tracking on or off for messages rendered from now on; messages already sent keep the links they were sent with. Only the fields sent change. Turn open tracking on only where your privacy notice covers it: open data is approximate (opensCaveat) and never a headline metric.
  • List experiments: List the A/B experiments in the caller's Nurture workspace, newest first, optionally filtered by kind (broadcast, journey), status (active, stopped, promoted), broadcastId or journeyId.
  • Get experiment: Read one experiment: what it tests (broadcast, or journey and split node), its arms, control, holdout and test shares, goal, minimum sample, status and, once promoted, the winner, who promoted it and whether significance was overridden (with the reason).
  • Create experiment: Set up an A/B test. kind broadcast: on a DRAFT broadcast, 2-5 arms { key, weight, templateVersion?, subject? } (an arm without overrides sends the broadcast as-is), optional holdoutPercent (0-50, receives nothing) and testPercent (1-99: only that share is tested at launch; the winner goes to the rest on promote). kind journey: journeyId and the nodeId of a split node in the draft; arms and holdout come from the node. Each contact's arm is hash(experiment id, contact id), recorded once. controlArm defaults to the first arm. Conversion, within goal.windowHours after exposure (send, split or withheld holdout): goal.kind event (default) = the lifecycle eventType (e.g. purchase); click = a tracked-link click on the experiment's own sends, only URLs starting with urlPrefix if set.
  • Get experiment results: Per arm: contacts assigned and exposed, conversions, conversion rate, uplift against the control (absolute and relative) and against the holdout, z and p-value. Statistics: a pooled two-proportion z-test of each variant against the control at a fixed 95% confidence level, alpha split across variants (Bonferroni); a comparison counts only when both arms reach minSamplePerArm exposures (default 100). decision is winner (recommendedWinner set), control_wins, insufficient_sample or no_significant_difference; a winner is never declared without significance. Conversion, within goal.windowHours after exposure (send, split or withheld holdout): goal.kind event (default) = the lifecycle eventType (e.g. purchase); click = a tracked-link click on the experiment's own sends, only URLs starting with urlPrefix if set.
  • Promote experiment winner: Make one arm the winner. It must be results.recommendedWinner (significant at 95%) unless override is true with an overrideReason; the override, reason and acting user are recorded. Broadcast with testPercent: the winner is queued for every remaining contact and the broadcast resumes sending (it must be sending or sent). Journey: the draft's split node is set to 100% winner (other arms weight 0, no holdout); publish the journey to make it live, as published versions never change. The experiment ends as promoted.
  • Stop experiment: Stop an active experiment without a winner. A broadcast experiment stopped before launch is ignored at launch; after launch, recipients already queued are still sent and the untested remainder is never sent. A journey keeps splitting by the node weights but stops recording assignments.
  • Draft with AI: Propose email copy (template_copy), subject lines (subject_lines) or a journey (journey) for a person to review, written by the Nexus completions gateway from goal and audience in the workspace's TopoloBrand voice. Only approved Brand claims may be stated; banned terms block the draft. templateId rewrites that template's copy; journey needs topicKey and may follow blueprintId (with segmentId). The draft is stored as proposed with guardrail findings and provenance; nothing is applied or published until app_topolo_nurture.drafts.approve.
  • List AI drafts: List the AI drafts in the caller's Nurture workspace, newest first (up to 200), optionally filtered by status (proposed, approved, rejected) and kind.
  • Get AI draft: Read one AI draft: its inputs, generated output, guardrail findings (error blocks approval; flag needs acknowledgement), approvable, provenance (Nexus route and model, Brand kit and voice version, claims version, prompt inputs hash, acting user) and, once approved, what was created.
  • Regenerate AI draft: Generate a proposed draft again from its stored inputs and the current Brand voice, optionally guided by reviewer feedback. The output, findings and provenance are replaced and attempt increases. Only proposed drafts can be regenerated; nothing is applied.
  • Approve AI draft: Approve a proposed draft as the calling person (recorded in decidedBy). Refused when a finding is an error, when flags exist without acknowledgeFlags true, or (with requireSecondApprover) when the caller requested it. Copy for a template becomes a NEW non-current version (make it current by saving it with app_topolo_nurture.templates.update); other copy becomes a new template; subject_lines applies candidate subjectIndex; a journey becomes a DRAFT journey with new templates. Nothing is published or sent.
  • Reject AI draft: Reject a proposed draft as the calling person, with an optional note. Nothing is applied; the draft is kept for audit.
  • Get AI drafting settings: Read the workspace's AI drafting approval policy: requireSecondApprover means the person who requested a draft cannot approve it.
  • Update AI drafting settings: Set whether AI drafts need a second person: with requireSecondApprover true, the requester of a draft cannot approve it.
  • Get sending settings: Read this workspace's Nexus sender profile, whether it is verified as a marketing-class sender (problems lists why not), the sender, queued sends per reason, and whether delivery outcomes come from Nexus events or polling. With no sender configured Nurture only records sends (nothing is emailed). Marketing attests the latest double opt-in, repermission or consent-decision record for the topic as Nexus consent; without one (import attestation only) it skips with no_consent_evidence. Background sends (journeys, broadcasts, confirmations from hosted pages) use the Nurture app service credential; only when it is missing do they wait in the outbox (queued.blocked_no_service_credential), and they are never dropped. deliveryEvents.mode is events once all seven nexus.email.* types are connected through event_sources.connect (sourceAppId app_topolo_nexus), else polling (deliveries.reconcile). Events cover background sends too: Nexus addresses a send with no person behind it to app:<app_id>.
  • Choose sender profile: Choose the Nexus email profile (an id from app_topolo_nexus.email.profiles.list) that this workspace sends as, or null to stop sending through Nexus (sends are then recorded only). A new profile starts unverified and nothing is emailed until sending_settings.verify passes.
  • Verify sender profile: Check the chosen profile through app_topolo_nexus.email.profiles.list, as the caller: it must exist, be active, use a provider that reports bounces and complaints (resend or ses), be on a domain Nexus verified, and send from a subdomain (e.g. news.example.com) so marketing reputation stays off the root domain. Saves verified or failed with the problems.
  • Reconcile delivery status: The fallback for workspaces without every Nexus delivery event connected: poll app_topolo_nexus.email.messages.get, as the caller, for recent sends (days 1-30, default 7; limit 1-100, default 50) whose final state is unknown. Delivered, bounced and complained become report engagement; hard bounces and complaints are added to the suppression ledger. When sending_settings.get shows deliveryEvents.mode events it polls nothing and returns skipped 1. Outcomes seen by both paths are recorded once.

Evidence

Actual dispatcher and migration-created SQLite or Worker handlers; identity/workspace and external service dependencies are simulated. Fixture identifiers and timestamps are synthetic. These responses establish no live authorization, paid generation, credential provisioning or message delivery.