Flodesk MCP tool field reference

Every parameter below is generated from the tool catalog the server advertises over MCP. Every returned field is generated from a contract traced to the upstream response struct or the connector line that derives it, and the same contract is what CI validates real handler output against.

Rates and shares are decimals between 0 and 1 (for example, 0.27 = 27%), except where a field explicitly states percentage points.

Reference format 3 · 35 tools · catalog sha256 7c6870e8b9c66a4d366fc4c0abc53504721dd7c85cf3de9e3c5a202ef50d05f0

add_subscriber_to_segment

Adds one subscriber to one static segment, idempotently.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
emailstringno—1–320 characters—
idstringno—1–320 characters—
segment_idstringyes—1–320 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A four-key receipt, relayed verbatim. The four simple write tools do NOT share a receipt shape: this one and `remove_subscriber_from_segment` carry a `segment`, `unsubscribe_subscriber` carries a status instead, and `update_subscriber` has no boolean at all.

FieldTypePresenceUnitsDerived in the connectorMeaning
addedbooleanalways present——True when this call created the membership, false when the subscriber was already in the segment. BOTH are successes — the guarded write is what decides, so `false` means "already there", never "failed".
subscriberobjectalways present——Just the two identifying keys — this receipt carries no status, no names and no memberships.
subscriber.idstringalways present——The subscriber id.
subscriber.emailstringalways present——Their email address.
segmentobjectalways present——The segment written to, so the name can be confirmed against the id that was sent.
segment.idstringalways present——The segment id.
segment.namestringalways present——The segment’s name.
messagestringalways present——A sentence stating the outcome — `Added <email> to segment "<name>".`, or `<email> is already in segment "<name>".` when `added` is false. Composed UPSTREAM, not by the connector.

structuredContent: Identical to the text payload — the same object is sent as `structuredContent`, so the key paths above are the paths a client reads on both channels.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

bulk_add_subscribers_to_segment

Queues a bulk job that adds every subscriber matching a filter to one static segment.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
confirmation_tokenstringyes—1–200 charactersFrom preview_bulk_change({ action: "add_to_segment" }) for this exact filter and segment. Single-use; expires 120 seconds after it is issued.
filterobjectyes——Required — a filter with 1–10 conditions; an unfiltered audience or count is not allowed.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
signup_formstringno—1–200 charactersA signup-form name, not an id.
segment_idstringyes—matching ^[0-9a-f]{24}$The segment id (24-character hex), as returned by list_segments.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A receipt for ENQUEUED work, or the statement that nothing was queued. It deliberately carries no job id and no `started` boolean: the id was a task-queue handle reaching users as a "Bulk action ID" line, and `started: true` read as completion to a model, which then reported the change as done before the job had run.

FieldTypePresenceUnitsDerived in the connectorMeaning
statusstring, one of: processing | waiting | nothing_to_doalways present—mapped from the upstream receipt status, or set to `nothing_to_do` on the empty-cohort path.Where the work stands, in customer terms rather than job terms. `processing` began immediately; `waiting` is behind another bulk action (one runs at a time, up to three queued); `nothing_to_do` means nothing matched at write time so NOTHING was queued. None is a completion — `processing` and `waiting` describe background work with no field here reporting what changed, and `nothing_to_do` is the one status that is terminal.
subscribers_matchedintegeralways presentwhole countthe connector counts it; never an upstream field.How many subscribers matched when the work was queued — counted fresh immediately before the enqueue, NOT read back out of the token record: the job enumerates the filter when it runs, so the fresh number names the group being acted on, and `previewed_before` carries the token-bound figure whenever the two differ. It is NOT a count of what changed: nothing has completed yet, and a subscriber already added to the segment is skipped. It is `0` exactly when `status` is `nothing_to_do`.
previewed_beforeintegerabsent when the fresh count equals the one the token bound — the ordinary case, on `processing` and `waiting`. It is ALWAYS present on `nothing_to_do`, where the approved figure is the only record of what the group was.whole countread from the stored token record, on any path where it differs from the fresh count.The cohort size a person approved earlier, present whenever it differs from what was there at write time. On `processing` and `waiting` it says the group moved after approval and by how much, in the direction the `message` names; growth past the allowed headroom never reaches here, having been refused. Alongside `subscribers_matched: 0` it is what distinguishes "the group emptied after you approved it" from "nothing matched in the first place", and on that path the confirmation token has been spent, so a fresh preview is needed.
filterobjectalways present—echoed from the parsed input.The STRUCTURED filter echoed back — not the compiled expression sent upstream — so the same selection can be reused or reversed. With `segment_id` it is the full record of who was added to which segment.
filter.matchstring, one of: all | anyalways present——Whether every condition or any condition had to match. Carries the schema default `all` when the caller omitted it, and is emitted before `conditions`.
filter.conditionsarray of objectalways present——The 1–10 conditions, echoed in the order they were supplied.
filter.conditions[].fieldstringalways present——The subscriber attribute or engagement metric compared.
filter.conditions[].operatorstring, one of: gt | ge | lt | le | eq | ne | containsalways present——The comparison operator, echoed as supplied. The legal set narrows by field: `gt`/`ge`/`lt`/`le` for dates and rates, plus `eq` for counts; `eq`/`ne` for `status` and `sourceType`; `contains` for `segmentIds` membership. Note `ge`/`le`, not `gte`/`lte`.
filter.conditions[].valuestring or numberalways present——The compared value: a string for dates, status and sourceType; a number for rates and engagement counts.
segment_idstringalways present—echoed from the parsed input.The segment subscribers were added to, echoed back. Always the id, never a name: names are resolved before this point, and a rule-computed segment is rejected rather than echoed.
signup_formstringabsent when the caller supplied no `signup_form` — the key is spread conditionally—echoed from the parsed input.The signup-form name echoed back when one scoped the cohort. It travels with the filter upstream, because the count was of `(filter) and sourceId in [form]`; sending the bare filter would have acted on a wider group than the number reported.
messagestringalways present—composed in the connector; no upstream origin.A plain-language sentence describing what is happening, composed here so a reader is not left to infer it from `status`. Its wording differs across all three statuses, and on the `nothing_to_do` path it additionally states that the confirmation token is spent, which is true of both halves because both redeem one.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

bulk_archive_subscribers

Applies a previously previewed bulk archive to the subscribers a filter matches, authorized by the token `preview_bulk_change` issued.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
confirmation_tokenstringyes—1–200 charactersFrom preview_bulk_change({ action: "archive" }). Single-use; expires 120 seconds after it is issued.
filterobjectyes——Required — a filter with 1–10 conditions; an unfiltered audience or count is not allowed.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
signup_formstringno—1–200 charactersA signup-form name, not an id.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A receipt for ENQUEUED work, never a report of what changed. It runs asynchronously in chunks, so nothing here counts how many subscribers were actually archived — `subscribers_matched` is the cohort size counted at queue time and neither `processing` nor `waiting` ever reaches a terminal state. This shape deliberately carries no job id and no `started` boolean: the id was a task-queue handle surfacing to users as a "Bulk action ID" line, and `started: true` read as completion to a model, which then reported the change as done before the job had run. Five keys, four when no signup form was used, six when the count moved after approval.

FieldTypePresenceUnitsDerived in the connectorMeaning
statusstring, one of: processing | waiting | nothing_to_doalways present—mapped from the upstream `started`/`queued` receipt status, or set to `nothing_to_do` on the empty-cohort path.Where the work stands, in customer terms rather than job terms. `processing` means it started immediately; `waiting` means it is behind another bulk action (one runs at a time, up to three queued); `nothing_to_do` means the cohort had emptied by write time, so NOTHING was queued. Only the last is terminal — the other two describe background work, and this response is a receipt, not a completion. There is no way to read a bulk action back, so nothing here can be polled.
subscribers_matchedintegeralways presentwhole countthe fresh drift-check count, not anything the enqueue response returns.How many subscribers matched when the work was queued — counted fresh immediately before the enqueue, NOT read back out of the token record: the job enumerates the filter when it runs, so the fresh number names the group being acted on. It is NOT a count of what has changed: nothing has completed yet, and a subscriber already in the target state, or inside its own 45-day window, is skipped. It is `0` exactly when `status` is `nothing_to_do`.
previewed_beforeintegerabsent when the fresh count equals the one the token bound — the ordinary case, on every statuswhole countread from the stored token record, on any path where it differs from the fresh count.The cohort size a person approved earlier, present only when it differs from what was there at write time. On `processing` and `waiting` it says the group moved after approval and by how much, in the direction the `message` names; growth past the allowed headroom never reaches here, having been refused. Alongside `subscribers_matched: 0` it says the group emptied after approval — and on that path the confirmation token has been spent, so a fresh preview is needed. It is the figure `expectedTotalItems` still carries upstream.
messagestringalways present—composed in the connector; no upstream origin.A plain-language sentence describing what is happening, composed here so a reader is not left to infer it from `status`. Its wording differs across all three statuses, and when the count moved after approval it names the earlier figure and the direction it moved in.
filterobjectalways present—echoed from the parsed input.The STRUCTURED filter echoed back — not the compiled expression sent upstream — so the same cohort can be re-previewed or unarchived again.
filter.matchstring, one of: all | anyalways present——Whether every condition or any condition had to match. Carries the schema default `all` when the caller omitted it, and is emitted before `conditions`.
filter.conditionsarray of objectalways present——The 1–10 conditions, echoed in the order they were supplied.
filter.conditions[].fieldstringalways present——The subscriber attribute or engagement metric compared.
filter.conditions[].operatorstring, one of: gt | ge | lt | le | eq | ne | containsalways present——The comparison operator, echoed as supplied. The legal set narrows by field: `gt`/`ge`/`lt`/`le` for dates and rates, plus `eq` for counts; `eq`/`ne` for `status` and `sourceType`; `contains` for `segmentIds` membership. Note `ge`/`le`, not `gte`/`lte`.
filter.conditions[].valuestring or numberalways present——The compared value: a string for dates, status and sourceType; a number for rates and engagement counts.
signup_formstringabsent when the caller supplied no `signup_form` — the key is spread conditionally—echoed from the parsed input.The signup-form name echoed back when one scoped the cohort. It travels with the filter upstream, because the previewed count was of `(filter) and sourceId in [form]`; sending the bare filter would act on a wider group than the token was issued for.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response. There is no per-subscriber result to page: the job is chunked upstream and its outcome is not readable through the connector. Every failure message says explicitly that nothing was archived, because the worst outcome is a partial or completed archive being reported that never happened.

Dates

No date inputs: the result is not scoped by a date window. Date-valued filter conditions take an absolute RFC-3339 timestamp or a relative `"-N days|weeks|months"` string, and must be byte-identical to the previewed filter.

Response variants

bulk_remove_subscribers_from_segment

Queues a bulk job that removes every subscriber matching a filter from one static segment.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
confirmation_tokenstringyes—1–200 charactersFrom preview_bulk_change({ action: "remove_from_segment" }) for this exact filter and segment. Single-use; expires 120 seconds after it is issued.
filterobjectyes——Required — a filter with 1–10 conditions; an unfiltered audience or count is not allowed.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
signup_formstringno—1–200 charactersA signup-form name, not an id.
segment_idstringyes—matching ^[0-9a-f]{24}$The segment id (24-character hex), as returned by list_segments.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A receipt for ENQUEUED work, or the statement that nothing was queued. It deliberately carries no job id and no `started` boolean: the id was a task-queue handle reaching users as a "Bulk action ID" line, and `started: true` read as completion to a model, which then reported the change as done before the job had run.

FieldTypePresenceUnitsDerived in the connectorMeaning
statusstring, one of: processing | waiting | nothing_to_doalways present—mapped from the upstream receipt status, or set to `nothing_to_do` on the empty-cohort path.Where the work stands, in customer terms rather than job terms. `processing` began immediately; `waiting` is behind another bulk action (one runs at a time, up to three queued); `nothing_to_do` means nothing matched at write time so NOTHING was queued. None is a completion — `processing` and `waiting` describe background work with no field here reporting what changed, and `nothing_to_do` is the one status that is terminal.
subscribers_matchedintegeralways presentwhole countthe connector counts it; never an upstream field.How many subscribers matched when the work was queued — counted fresh immediately before the enqueue, NOT read back out of the token record: the job enumerates the filter when it runs, so the fresh number names the group being acted on, and `previewed_before` carries the token-bound figure whenever the two differ. It is NOT a count of what changed: nothing has completed yet, and a subscriber already removed from the segment is skipped. It is `0` exactly when `status` is `nothing_to_do`.
previewed_beforeintegerabsent when the fresh count equals the one the token bound — the ordinary case, on `processing` and `waiting`. It is ALWAYS present on `nothing_to_do`, where the approved figure is the only record of what the group was.whole countread from the stored token record, on any path where it differs from the fresh count.The cohort size a person approved earlier, present whenever it differs from what was there at write time. On `processing` and `waiting` it says the group moved after approval and by how much, in the direction the `message` names; growth past the allowed headroom never reaches here, having been refused. Alongside `subscribers_matched: 0` it is what distinguishes "the group emptied after you approved it" from "nothing matched in the first place", and on that path the confirmation token has been spent, so a fresh preview is needed.
filterobjectalways present—echoed from the parsed input.The STRUCTURED filter echoed back — not the compiled expression sent upstream — so the same selection can be reused or reversed. With `segment_id` it is the full record of who was removed from which segment.
filter.matchstring, one of: all | anyalways present——Whether every condition or any condition had to match. Carries the schema default `all` when the caller omitted it, and is emitted before `conditions`.
filter.conditionsarray of objectalways present——The 1–10 conditions, echoed in the order they were supplied.
filter.conditions[].fieldstringalways present——The subscriber attribute or engagement metric compared.
filter.conditions[].operatorstring, one of: gt | ge | lt | le | eq | ne | containsalways present——The comparison operator, echoed as supplied. The legal set narrows by field: `gt`/`ge`/`lt`/`le` for dates and rates, plus `eq` for counts; `eq`/`ne` for `status` and `sourceType`; `contains` for `segmentIds` membership. Note `ge`/`le`, not `gte`/`lte`.
filter.conditions[].valuestring or numberalways present——The compared value: a string for dates, status and sourceType; a number for rates and engagement counts.
segment_idstringalways present—echoed from the parsed input.The segment subscribers were removed from, echoed back. Always the id, never a name: names are resolved before this point, and a rule-computed segment is rejected rather than echoed.
signup_formstringabsent when the caller supplied no `signup_form` — the key is spread conditionally—echoed from the parsed input.The signup-form name echoed back when one scoped the cohort. It travels with the filter upstream, because the count was of `(filter) and sourceId in [form]`; sending the bare filter would have acted on a wider group than the number reported.
messagestringalways present—composed in the connector; no upstream origin.A plain-language sentence describing what is happening, composed here so a reader is not left to infer it from `status`. Its wording differs across all three statuses, and on the `nothing_to_do` path it additionally states that the confirmation token is spent, which is true of both halves because both redeem one.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

bulk_unarchive_subscribers

Applies a previously previewed bulk unarchive to the subscribers a filter matches, authorized by the token `preview_bulk_change` issued.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
confirmation_tokenstringyes—1–200 charactersFrom preview_bulk_change({ action: "unarchive" }). Single-use; expires 120 seconds after it is issued.
filterobjectyes——Required — a filter with 1–10 conditions; an unfiltered audience or count is not allowed.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
signup_formstringno—1–200 charactersA signup-form name, not an id.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A receipt for ENQUEUED work, never a report of what changed. It runs asynchronously in chunks, so nothing here counts how many subscribers were actually unarchived — `subscribers_matched` is the cohort size counted at queue time and neither `processing` nor `waiting` ever reaches a terminal state. This shape deliberately carries no job id and no `started` boolean: the id was a task-queue handle surfacing to users as a "Bulk action ID" line, and `started: true` read as completion to a model, which then reported the change as done before the job had run. Five keys, four when no signup form was used, six when the count moved after approval.

FieldTypePresenceUnitsDerived in the connectorMeaning
statusstring, one of: processing | waiting | nothing_to_doalways present—mapped from the upstream `started`/`queued` receipt status, or set to `nothing_to_do` on the empty-cohort path.Where the work stands, in customer terms rather than job terms. `processing` means it started immediately; `waiting` means it is behind another bulk action (one runs at a time, up to three queued); `nothing_to_do` means the cohort had emptied by write time, so NOTHING was queued. Only the last is terminal — the other two describe background work, and this response is a receipt, not a completion. There is no way to read a bulk action back, so nothing here can be polled.
subscribers_matchedintegeralways presentwhole countthe fresh drift-check count, not anything the enqueue response returns.How many subscribers matched when the work was queued — counted fresh immediately before the enqueue, NOT read back out of the token record: the job enumerates the filter when it runs, so the fresh number names the group being acted on. It is NOT a count of what has changed: nothing has completed yet, and a subscriber already in the target state, or inside its own 45-day window, is skipped. It is `0` exactly when `status` is `nothing_to_do`.
previewed_beforeintegerabsent when the fresh count equals the one the token bound — the ordinary case, on every statuswhole countread from the stored token record, on any path where it differs from the fresh count.The cohort size a person approved earlier, present only when it differs from what was there at write time. On `processing` and `waiting` it says the group moved after approval and by how much, in the direction the `message` names; growth past the allowed headroom never reaches here, having been refused. Alongside `subscribers_matched: 0` it says the group emptied after approval — and on that path the confirmation token has been spent, so a fresh preview is needed. It is the figure `expectedTotalItems` still carries upstream.
messagestringalways present—composed in the connector; no upstream origin.A plain-language sentence describing what is happening, composed here so a reader is not left to infer it from `status`. Its wording differs across all three statuses, and when the count moved after approval it names the earlier figure and the direction it moved in.
filterobjectalways present—echoed from the parsed input.The STRUCTURED filter echoed back — not the compiled expression sent upstream — so the same cohort can be re-previewed or archived again.
filter.matchstring, one of: all | anyalways present——Whether every condition or any condition had to match. Carries the schema default `all` when the caller omitted it, and is emitted before `conditions`.
filter.conditionsarray of objectalways present——The 1–10 conditions, echoed in the order they were supplied.
filter.conditions[].fieldstringalways present——The subscriber attribute or engagement metric compared.
filter.conditions[].operatorstring, one of: gt | ge | lt | le | eq | ne | containsalways present——The comparison operator, echoed as supplied. The legal set narrows by field: `gt`/`ge`/`lt`/`le` for dates and rates, plus `eq` for counts; `eq`/`ne` for `status` and `sourceType`; `contains` for `segmentIds` membership. Note `ge`/`le`, not `gte`/`lte`.
filter.conditions[].valuestring or numberalways present——The compared value: a string for dates, status and sourceType; a number for rates and engagement counts.
signup_formstringabsent when the caller supplied no `signup_form` — the key is spread conditionally—echoed from the parsed input.The signup-form name echoed back when one scoped the cohort. It travels with the filter upstream, because the previewed count was of `(filter) and sourceId in [form]`; sending the bare filter would act on a wider group than the token was issued for.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response. There is no per-subscriber result to page: the job is chunked upstream and its outcome is not readable through the connector. Every failure message says explicitly that nothing was unarchived, because the worst outcome is a partial or completed unarchive being reported that never happened.

Dates

No date inputs: the result is not scoped by a date window. Date-valued filter conditions take an absolute RFC-3339 timestamp or a relative `"-N days|weeks|months"` string, and must be byte-identical to the previewed filter.

Response variants

create_segment

Creates a static segment from a structured filter, fills it once from that filter, and reports how many subscribers were queued into it.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
namestringyes—1–100 charactersThe new segment name, passed directly. A collision with an existing segment name comes back as an error.
filterobjectyes——Required — a filter with 1–10 conditions; an unfiltered audience or count is not allowed.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
signup_formstringno—1–200 charactersA signup-form name, not an id.
confirmation_tokenstringno—1–200 charactersRequired. From preview_segment_count for this exact filter and signup form. Single-use; expires 120 seconds after it is issued.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. Exactly two top-level keys. Unlike the bulk tools, this one echoes back neither `filter` nor `signup_form`, and it MINTS no token — but it REDEEMS one: the create is refused outright without a `confirmation_token` from `preview_segment_count` for the same cohort. The bulk-action receipt for the fill is deliberately not surfaced: it is a task-queue handle with no use to the caller.

FieldTypePresenceUnitsDerived in the connectorMeaning
segmentobjectalways present——Exactly three keys. The contract for this object is CLOSED on the connector side, so a new nested backend key is dropped rather than relayed until it is reviewed and described.
segment.idstringalways present——The new segment’s id — usable immediately with `inspect_segment`, and the id the one-time fill was queued against.
segment.namestringalways present——The new segment’s name, as stored.
segment.matchedCountintegeralways presentwhole count—How many subscribers matched the filter, measured immediately BEFORE the create by the same count query `preview_segment_count` runs, and then queued into the segment — the same frame `bulk_add_subscribers_to_segment` reports as `subscribers_matched`. It is what was QUEUED, not settled membership: the fill is chunked and asynchronous and has not finished when the call returns, so a read moments later can show fewer. `0` is a valid success — the segment is created empty and no fill is queued.
messagestringalways present——A sentence confirming the create and its size — `Created segment "X" with N matching subscribers.` — followed by what the fill did and the standing snapshot caveat: `Membership is a one-time snapshot taken at creation and does not change as subscribers start or stop matching.` Composed by the CONNECTOR, which appends its own sentences to the upstream create message, because the fill is enqueued MCP-side and only the connector knows whether it was queued. Two branches, both success: a queued fill and a zero cohort created empty.

structuredContent: Identical to the text payload — the same object is sent as `structuredContent`, so the key paths above are the paths a client reads on both channels.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window. Date-valued filter conditions take an absolute RFC-3339 timestamp or a relative `"-N days|weeks|months"` string; a bare date is rejected.

Response variants

create_subscriber

Creates one subscriber, or returns the existing one unchanged when the email is already on the list.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
emailstringyes—1–320 characters—
first_namestringno—0–100 characters—
last_namestringno—0–100 characters—
segment_idsarray of stringno—0–20 items; each item 1–320 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A find-or-create receipt. Any extra top-level key the upstream might add is dropped by an explicit destructure, so this is the whole payload.

FieldTypePresenceUnitsDerived in the connectorMeaning
createdbooleanalways present——True when this call created the subscriber, false when one already existed and was returned unchanged. This flag is authoritative — the connector cannot see the HTTP status, so it is the only signal that distinguishes the two outcomes, and BOTH are successes.
subscriberobjectalways present——The create-path profile, which is NARROWER than `get_subscriber`’s: the upstream projection omits `createdAt`, `source`, `lastOpenedAt`, `lastClickedAt` and `customFields`. Call `get_subscriber` when those are needed.
subscriber.idstringalways present——The subscriber id.
subscriber.emailstringalways present——Their email address.
subscriber.firstNamestringalways present——First name, `""` when unset.
subscriber.lastNamestringalways present——Last name, `""` when unset.
subscriber.statusstring, one of: active | unconfirmed | unsubscribed | bounced | complained | cleaned | archivedalways present——Subscription state after the call, from the same roster the read tools return; `active` is the backend’s `confirmed`.
subscriber.segmentsarray of objectalways present——Segment memberships after the call, `[]` when none — never null.
subscriber.segments[].namestringalways present——The segment’s name.
subscriber.segments[].typestring, one of: static | dynamicalways present——Only `static` or `dynamic` on the subscriber routes — a `pre_built` segment is reported as `dynamic` here, unlike `list_segments` and `inspect_segment`, which return the raw type.
notestringconditional — `created` is false AND the call also supplied a name or `segment_ids` — because neither is applied to an existing subscriber. Absent otherwise, never null—composed in fd-mcp-server, with no upstream origin.A warning that the requested names or segments were NOT applied, naming `update_subscriber` and `add_subscriber_to_segment` as the tools that would apply them. Its presence means part of the request was silently not performed.

structuredContent: Identical to the text payload — the same object is sent as `structuredContent`, so the key paths above are the paths a client reads on both channels.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

export_subscribers

Queues a CSV export of a subscriber group and reports how many it covers; the file itself is emailed to the account, never returned here.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
confirmation_tokenstringyes—1–200 charactersFrom preview_bulk_change({ action: "export" }) for this exact group. Single-use; expires 120 seconds after it is issued.
filterobjectno——An ad-hoc group to export. Pass this or `segment_id`, never both and never neither.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
segment_idstringno—1–64 charactersThe id of one segment, as returned by `list_segments`. Pass this or `filter`, never both and never neither.
signup_formstringno—1–200 charactersA signup-form name, not an id. Valid only alongside `filter`.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. Three always-present keys plus the echoed group. The upstream job id is deliberately NOT relayed: no tool consumes it, and an operational id does not belong in a tool result.

FieldTypePresenceUnitsDerived in the connectorMeaning
startedbooleanalways present—asserted by the connector, not relayed.Always `true` on the success path — the export job was ACCEPTED, not finished. There is no false value: a refusal is an `isError` result instead.
matchedintegeralways presentwhole count—How many subscribers the export covers, counted live immediately BEFORE the job was queued, over the same resolved filter string the job is given. It is never 0 on this path: a zero cohort is refused instead, because the upstream exporter returns early without publishing a completion event and no email would ever arrive.
previewed_beforeintegerconditional — the count the job was given differs from the one the confirmation token was issued for. Absent in the ordinary case where the two agree, and never nullwhole countread from the redeemed token record, never an upstream value.The group size a person approved at preview time, present only when the group moved between then and the enqueue. Only a SHRINK reaches here: growth past the drift headroom is refused rather than exported, and a shrink to nothing is refused too. It exists so a file covering fewer people than were approved is disclosed rather than silently substituted.
messagestringalways present—composed from `matched`.A sentence stating the count, that the file is being emailed to the address on the account, and that the download link stops working 24 hours after it is sent. Composed by the connector, because the response carries no file and no link — without it a reader has nowhere to look for the export.
filterobjectconditional — the caller selected the group with `filter`. Absent when `segment_id` was used, never null——The filter the export was scoped to, echoed as the structured input so the same group can be re-exported without rebuilding it.
filter.matchstring, one of: all | anyalways present——Whether every condition or any condition had to match. Carries the schema default `all` when the caller omitted it, and is emitted before `conditions`.
filter.conditionsarray of objectalways present——The 1–10 conditions, echoed in the order they were supplied.
filter.conditions[].fieldstringalways present——The subscriber attribute or engagement metric compared.
filter.conditions[].operatorstring, one of: gt | ge | lt | le | eq | ne | containsalways present——The comparison operator, echoed as supplied. The legal set narrows by field: `gt`/`ge`/`lt`/`le` for dates and rates, plus `eq` for counts; `eq`/`ne` for `status` and `sourceType`; `contains` for `segmentIds` membership. Note `ge`/`le`, not `gte`/`lte`.
filter.conditions[].valuestring or numberalways present——The compared value: a string for dates, status and sourceType; a number for rates and engagement counts.
signup_formstringconditional — the caller supplied a `signup_form`. Absent otherwise, never null——The signup-form NAME the group was narrowed by. It is resolved to a source id upstream and ANDed onto the filter before the cohort is counted, so the number in `matched` and the rows in the file are scoped by it too.
segment_idstringconditional — the caller selected the group with `segment_id`. Absent when `filter` was used, never null——The segment the export was scoped to, echoed back. Exactly one of `filter` or `segment_id` is ever present, mirroring the input rule.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response. The export itself is not capped either: the job pages, writes and merges the whole group however large, so nothing is truncated.

Dates

No date inputs: the result is not scoped by a date window. Date-valued filter conditions take an absolute RFC-3339 timestamp or a relative `"-N days|weeks|months"` string; a bare date is rejected.

Response variants

flodesk_overview

Returns a static routing guide naming the connector’s tools by capability area, so a plan can be formed without an exploratory data call.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: string (markdown), always present. A markdown routing guide listing the connector’s tools under capability headings (email performance, subscribers, workflows and automations, forms, checkout and revenue, account). It carries no account data of any kind, so it answers "which tool" without spending a data call.

structuredContent: An object with the single key `guide`, carrying the same payload one level deeper — so every field above is at `guide.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

get_brand_settings

Returns the connected Flodesk account’s brand identity — the name, website, logo, address, palette, fonts and social links an LLM needs to produce on-brand output.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. The account’s brand identity. Deliberately excluded from the underlying record: the terms-and-conditions and privacy-policy HTML blobs, the double-optin ids, the viral-footer and free-month account flags, and the record’s own ids and timestamps.

FieldTypePresenceUnitsDerived in the connectorMeaning
namestringabsent when The account has not set it. Upstream marks every field `omitempty`, so an unset field is absent rather than empty — nothing here is ever `""`, `{}` or `null`.——The business/brand name as typed in Brand settings. Not the account owner’s personal name — that is `fullName` on `get_me`.
websitestringabsent when The account has not set it. Upstream marks every field `omitempty`, so an unset field is absent rather than empty — nothing here is ever `""`, `{}` or `null`.——The brand’s website URL.
logostringabsent when The account has not set it. Upstream marks every field `omitempty`, so an unset field is absent rather than empty — nothing here is ever `""`, `{}` or `null`.——Direct image URL for the brand logo. Stored and returned verbatim, so it may carry crop/resize query parameters that render the logo as the user cropped it; the connector neither parses nor rewrites them.
addressobjectabsent when Every part of the address is unset.——The mailing address Flodesk renders in email footers. Parts are individually optional — a brand may set a country but no street.
address.streetstringabsent when The account has not set it. Upstream marks every field `omitempty`, so an unset field is absent rather than empty — nothing here is ever `""`, `{}` or `null`.——Street line of the mailing address; may itself carry a suite or unit.
address.citystringabsent when The account has not set it. Upstream marks every field `omitempty`, so an unset field is absent rather than empty — nothing here is ever `""`, `{}` or `null`.——City or town of the mailing address.
address.statestringabsent when The account has not set it. Upstream marks every field `omitempty`, so an unset field is absent rather than empty — nothing here is ever `""`, `{}` or `null`.——State, province or region, as the account typed it. Not a code — free text.
address.countrystringabsent when The account has not set it. Upstream marks every field `omitempty`, so an unset field is absent rather than empty — nothing here is ever `""`, `{}` or `null`.——Country name as the account typed it. Not an ISO code — `get_me` carries the account country code.
address.postalCodestringabsent when The account has not set it. Upstream marks every field `omitempty`, so an unset field is absent rather than empty — nothing here is ever `""`, `{}` or `null`.——Postal or ZIP code of the mailing address.
colorsobject with dynamic keysabsent when The account has set no colors, or has cleared every slot it set.——Brand palette, hex values. Sparse — only the slots the user filled appear. The keys are positional slots (`color1`, `color2`, …) and carry NO semantic role: there is no primary/secondary/accent meaning to read into them. The upstream column is an open `map[string]string` whose key validator constrains nothing, so treat the slot set as open rather than a fixed range — read the keys present rather than probing for a slot number. A slot cleared in the app is stored as an empty string upstream and filtered out, so a present key is always a real color.
colors.<key>stringone entry per key present in the payload——A palette slot, e.g. `color1`, `color2` — an open set, not a fixed range.
socialLinksobject with dynamic keysabsent when The account has set no social links, or has cleared every one it set.——Profile URLs keyed by network. Sparse — only the networks the user set appear. The upstream column is an open `map[string]string` with no network enumeration behind it, so read the keys present rather than probing for a known set; `twitter` is the key for X. A link cleared in the app is filtered out the same way colors are.
socialLinks.<key>stringone entry per key present in the payload——A social network name, e.g. `instagram`, `tiktok`, `substack`.
fontFamiliesarray of stringabsent when The account has uploaded no custom fonts — the common case.——Font family names uploaded to this account, sorted. Absent does NOT mean the brand has no typography: it means no CUSTOM fonts were uploaded, and the account uses Flodesk’s built-in fonts. Weights, styles and font-file URLs are deliberately not carried. Capped at the 200 families the upstream page size allows.
messagestringconditional — Emitted by fd-mcp-server, and only when the account has configured nothing at all. It is then the ONLY key: a bare `{}` reads to a model as a failed call, so the empty case is narrated instead.—composed in the connector when the upstream body is empty; never an upstream field.Says no brand settings are configured yet and where to set them. Never present alongside brand data.

structuredContent: An object with the single key `brand`, carrying the same payload one level deeper — so every field above is at `brand.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

get_checkout_totals

Returns Flodesk Checkout revenue and conversion — the account summary, the per-checkout breakdown, and the split by payment type.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
currencystringno—matching ^[A-Z]{3}$ISO 4217 3-letter uppercase code (e.g. USD). Omit to use the account default; the response `currencies` field lists the currencies available on the account.
order_bystringno—one of: name | status | totalOrders | totalVisitors | totalSales | convRateSort key for the per-checkout breakdown. Defaults to totalOrders when omitted.
sortstringno—one of: asc | desc—
pageintegerno—1 to ∞—
per_pageintegerno—1 to 20—
is_subscriptionbooleanno———
fromstringno—1–64 characters—
tostringno—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. Exactly four top-level keys — `overall`, `funnels`, `revenueMix`, `currencies` — joining three upstream responses. The only value transformation is the money conversion; nothing is renamed or dropped.

FieldTypePresenceUnitsDerived in the connectorMeaning
overallobjectalways present—`overall` is a key name fd-mcp-server gives the first upstream response.The account-wide checkout summary, always complete and never paged.
overall.totalVisitorsintegeralways presentwhole count—Visitors across checkouts.
overall.totalOrdersintegeralways presentwhole count—Orders placed.
overall.totalSubscriptionOrdersintegeralways presentwhole count—Orders that were subscriptions.
overall.totalSalesnumberalways presentthe currency’s major unit (e.g. dollars, not cents) — converted here from the upstream minor-unit integerapplied at — upstream sends minor units (cents); the connector divides by 100 except for zero-decimal currencies (jpy, krw, vnd and the rest of that roster), using the nearest enclosing `currency` value.Gross sales. The money key is `totalSales` — there is NO `totalRevenue` anywhere in this payload.
overall.totalSubscriptionRevenuenumberalways presentthe currency’s major unit (e.g. dollars, not cents) — converted here from the upstream minor-unit integerapplied at — upstream sends minor units (cents); the connector divides by 100 except for zero-decimal currencies (jpy, krw, vnd and the rest of that roster), using the nearest enclosing `currency` value.The subscription share of gross sales.
overall.totalPublishesintegeralways presentwhole count—Published checkouts.
overall.totalCustomersintegeralways presentwhole count—Distinct customers.
overall.convRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Orders divided by visitors. The rate key is `convRate` — there is NO `conversionRate`.
overall.currencystringalways presentlowercase ISO 4217 code—The SINGLE currency every money figure above is denominated in: the `currency` input when given, otherwise the account’s first available currency. `""` when the account has none.
overall.currenciesarray of stringalways present; null when the account has no currencies at allISO 4217 codes—The account’s roster of available currencies — the values the `currency` input accepts. Distinct from `currency` (singular), which is the one resolved for this response. Declared upstream as a string array with NO `omitempty`, so the key is always present and a nil slice marshals to `null`.
funnelsobjectalways present—`funnels` is a key name fd-mcp-server gives the second upstream response.The per-checkout breakdown, kept in its own upstream envelope — so the rows are at `funnels.data[]`, NOT at `funnels[]`.
funnels.dataarray of objectalways present——The ranked page of checkouts, `[]` when empty — never null.
funnels.data[].idstringalways present——The checkout id.
funnels.data[].namestringalways present——The checkout’s name.
funnels.data[].statusstringalways present——The checkout’s publication status as stored upstream.
funnels.data[].totalVisitorsintegeralways presentwhole count—Visitors to this checkout.
funnels.data[].totalOrdersintegeralways presentwhole count—Orders through this checkout.
funnels.data[].totalSalesnumberalways presentthe currency’s major unit (e.g. dollars, not cents) — converted here from the upstream minor-unit integerapplied at — upstream sends minor units (cents); the connector divides by 100 except for zero-decimal currencies (jpy, krw, vnd and the rest of that roster), using the nearest enclosing `currency` value. This row converts using the ROW’s own `currency`, not the top-level one.Gross sales for this checkout.
funnels.data[].totalCustomersintegeralways presentwhole count—Distinct customers for this checkout.
funnels.data[].totalActiveSubscriptionsintegeralways presentwhole count—Active subscriptions from this checkout. Note the name: `totalActiveSubscriptions` here, `totalActives` on the workflow and segment rankings.
funnels.data[].convRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—This checkout’s orders divided by its visitors.
funnels.data[].currencystringalways presentlowercase ISO 4217 code—This row’s currency. It is load-bearing: the row’s money figure is converted with it.
revenueMixobjectalways present—`revenueMix` is a key name fd-mcp-server gives the third upstream response.The revenue split by payment type.
revenueMix.totalSalesnumberalways presentthe currency’s major unit (e.g. dollars, not cents) — converted here from the upstream minor-unit integerapplied at — upstream sends minor units (cents); the connector divides by 100 except for zero-decimal currencies (jpy, krw, vnd and the rest of that roster), using the nearest enclosing `currency` value.Gross sales across the three payment types.
revenueMix.totalOrdersintegeralways presentwhole count—Orders across the three payment types.
revenueMix.currencystringalways presentlowercase ISO 4217 code—The currency this breakdown is denominated in.
revenueMix.itemsarray of objectalways present——Always EXACTLY three rows in a fixed order — one-time, payment plan, subscription — with zeros for a type that had no orders. Never a short array.
revenueMix.items[].typestring, one of: one_time | payment_plan | subscriptionalways present——The payment type this row aggregates.
revenueMix.items[].namestringalways present——The display label — `One-time payments`, `Payment plans` or `Subscriptions`. Always set, even for a type with no orders.
revenueMix.items[].totalOrdersintegeralways presentwhole count—Orders of this payment type.
revenueMix.items[].totalSalesnumberalways presentthe currency’s major unit (e.g. dollars, not cents) — converted here from the upstream minor-unit integerapplied at — upstream sends minor units (cents); the connector divides by 100 except for zero-decimal currencies (jpy, krw, vnd and the rest of that roster), using the nearest enclosing `currency` value. These rows carry no `currency` of their own, so they inherit `revenueMix.currency`.Gross sales of this payment type.
revenueMix.items[].percentnumberalways presentPERCENTAGE POINTS from 0 to 100 with two decimals (42.31 = 42.31%) — deliberately unlike `convRate`, which is a 0–1 decimal. The two scales coexist in this one payload.—This type’s share of `revenueMix.totalSales`. The last row absorbs the rounding remainder so the three sum to 100.
currenciesarray of stringalways present; null when the account has no currencies at allISO 4217 codesduplicated in fd-mcp-server from `overall.currencies`.A copy of `overall.currencies`, lifted to the top level for convenience. Same values, same nullness — `null`, never absent, because the upstream field it is copied from is always present.

structuredContent: An object with the single key `checkout`, carrying the same payload one level deeper — so every field above is at `checkout.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Paged by `page` (1-based) and `per_page` (1–20; no advertised default — the backend uses 10 and silently resets anything outside 1–20 to 10). The upstream writes `X-Total`, `X-Total-Pages`, `X-Page` and `X-Per-Page` as HTTP response headers and the connector’s client returns only the parsed body, so the response carries NO `total`, `page`, `perPage` or `hasMore` field. A short or empty `data` array is the only end-of-collection signal, and an empty page cannot be told apart from a page past the end. Only `funnels.data` is paged; `overall` and `revenueMix` are always complete.

Dates

Accepts `from` and `to` as either a bare date (`2026-09-01`) or a full RFC-3339 timestamp. `from` is coerced to start-of-day and `to` to end-of-day, in the window the backend compares; an omitted `to` defaults to today. `from` must be on or before `to`, and the resolved span must not exceed 365 days — both are client-side validation errors, not upstream ones. Omitting `from` selects the lifetime path and sends no window upstream.

Response variants

get_email_content

Returns what one email actually said — its copy as ordered blocks, with link destinations and image urls — plus the template it was built on.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
email_idstringyes—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. One email’s content and the design it was built on. The rendered HTML is never returned; the copy is extracted from the editor’s stored blocks.

FieldTypePresenceUnitsDerived in the connectorMeaning
idstringalways present——The email’s id, echoing the one asked for.
namestringalways present——The internal name of the campaign, which is what the account owner sees in their list — not the subject line.
statusstringalways present——The campaign’s stored status. A sent campaign reads `done`; drafts read `draft` and scheduled sends `scheduled`. There is no `sent` value.
subjectstringalways present——The subject line as the recipient received it — editor markup stripped and entities decoded by the same helper `rank_emails` uses, so the two tools cannot disagree. `""` on a draft whose subject was never set.
previewTextstringalways present——The preview/preheader text, `""` when unset.
sendTimestring (rfc3339)always present; null when the campaign is a draft that was never scheduled——When the campaign was sent, or is scheduled to send. The key is ALWAYS present and carries JSON `null` for a draft with neither — deliberately not the zero time, which is what the underlying value would otherwise marshal to.
blocksarray of objectalways present——The email’s content in the order it appears, so the first entry is the opening hook. Blocks that are pure layout or chrome — spacers, dividers, the logo, social icons, the address block and the footer — never appear, because they carry no copy. `[]` for a campaign whose body is empty or unreadable, which is a normal read and not an error.
blocks[].typestringalways present——The kind of block the copy came from, as the email editor stored it — for example `art`, `text`, `button`, `image`. It is the editor’s own vocabulary, not a fixed enum, so a block kind added to the editor appears here unchanged.
blocks[].textstringabsent when the block carries no copy — an image block is the usual case——The block’s copy with formatting removed, entities decoded and whitespace collapsed. Paragraphs within one block are separated by newlines. Merge tags such as `{{first_name}}` are preserved verbatim, because they are part of what the author wrote.
blocks[].hrefstringabsent when the block links nowhere——Where the block points — the destination of a button or a linked element, so a call to action is legible as more than its label.
blocks[].linkTypestringabsent when the block links nowhere——What kind of destination `href` is, as stored by the editor — for example an external url, an uploaded file, or a checkout.
blocks[].imageUrlstringabsent when the block is not an image——The image’s source url. An image block usually carries no `text` — alt text or a caption surfaces as text when the block has one, but words set inside the picture itself are not stored as text anywhere. So an email whose blocks are mostly images is one whose copy is largely unreadable, not one with little to say.
totalBlocksintegeralways present——How many content blocks the email has — layout-only blocks are never counted. It exceeds the length of `blocks` whenever the budget dropped whole blocks, but not when a single oversized block was returned as a truncated prefix; `blocksTruncated` covers both.
blocksTruncatedbooleanalways present——Whether `blocks` is a prefix of the email rather than the whole of it. The cut normally lands on a block boundary, so copy is not severed mid-sentence — except for a single block longer than the whole budget, which is returned as a truncated prefix of its own text rather than dropped (dropping it would read as an empty email). This flag, not a `totalBlocks` that exceeds the returned count, is the reliable signal that a read is partial.
templateobjectalways present——The design the email was built on.
template.sourcestringalways present——Where the design came from — a gallery template, a private gallery template, a template the user saved, a scratch build, Canva, or Studio. Empty for a campaign created by duplicating another one, which records no source of its own.
template.idstringabsent when the campaign referenced no template — built from scratch, or duplicated from another campaign——The stable identifier of the template the email was built on. This is what groups campaigns by design; `rank_emails` returns the same identifier on every ranked row, so grouping many emails by template needs no call to this tool.
template.namestringabsent when the referenced template record carries no name — a template the user saved themselves has none, and neither does a gallery template that has since been deleted——The template’s display name. Absence is a fact about the stored record, never a failed lookup: the identifier is still returned and is sufficient to group by design.

structuredContent: An object with the single key `results`, carrying the same payload one level deeper — so every field above is at `results.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

get_email_stats

Returns one sent email’s performance by id, including per-variant results and the winner when that email ran a subject-line split test.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
email_idstringyes—1–100 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. One sent email’s blended performance, with its subject-line A/B test attached when it ran one.

FieldTypePresenceUnitsDerived in the connectorMeaning
idstringalways present——The email id. A resend resolves to its PARENT’s id upstream, so the value can differ from the `email_id` that was asked for. When it does, the response MIXES two campaigns: the counts and rates are the resend’s own, while this id, `subject` and `abTest` are the original’s — and `name` ends in ` - Resend`. Re-reading with this id returns the original’s own figures.
namestringalways present——The internal email name; a resend reads `"<name> - Resend"`.
sentAtstring (rfc3339)always present——When the email was sent.
subjectstringalways present——The subject line as a recipient saw it, `""` when it could not be resolved — parsed and rendered, so neither editor markup nor personalization-token template source reaches the caller. The same helper serves `rank_emails` and the per-variant subjects, so those reads cannot disagree about one email. On a FINALIZED split test this is the winning variant’s subject: the campaign subject is overwritten when a winner is picked (variant A’s on a `tie`), and left alone only when the winner is `none`. The sample group saw two different subjects; `variants[]` carries both.
totalSendsintegeralways presentwhole count—Individual sends across every group that has sent. Before the remainder ships — a running or cancelled test — this tracks the two variants combined; once it ships it exceeds them. Both this and the per-variant `sent` values are counted the same way over the same event log — identical DISTINCT-subscriber expressions — so they are not different grains and nothing is rounded. What differs is the store: this figure is read from a periodically-refreshed rollup while the per-variant figures are read live off the event log (and frozen once a test completes). The rollup is recomputed by a batch job with no bounded cadence, so it can trail them — occasionally by a long way. A figure BELOW the two variants’ combined `sent` proves the rollup is behind; one at or above it does not prove it is current, since once the remainder ships this one exceeds them by design. The email’s report page in Flodesk counts live, so it can show a higher figure than this one. A resend does not raise that page’s send count — it reaches only the original’s unopened recipients, who are already counted — so for this figure the gap is lag alone, even for a resent email.
totalUniqueOpensintegeralways presentwhole count—Distinct subscribers who opened, blended across the whole send. For a resent email the report page in Flodesk also counts the resend’s openers, none of whom had opened the original, so it stays above this figure even after the rollup catches up; `rank_emails` reports the resend as its own ` - Resend` row.
openRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique opens divided by deliveries across every group that has sent. The blend spans THREE groups — variant A, variant B, and the remainder — so it is not bounded by the two variant rates and may sit above, below, or between them. A blend below both variant rates and one above both are each reachable, and both are exercised by the committed cases. It is also read from the periodically-refreshed rollup while the per-variant rates are read live, so the two can disagree on freshness alone — most visibly during a running test. Nothing bounds the rollup’s age: a batch backlog can leave it behind the live figures beside it — occasionally by a long way.
totalUniqueClicksintegeralways presentwhole count—Distinct subscribers who clicked, blended across the whole send. For a resent email the report page in Flodesk also counts the resend’s clickers, so it stays above this figure even after the rollup catches up.
clickRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Blended unique clicks divided by deliveries.
unsubRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unsubscribes divided by deliveries, blended across the whole send.
abTestobjectalways present; null when the email ran no subject-line split test — the case for most campaigns——The split test attached to this email. `null` is the explicit "there was no test" signal, and no variant data of any kind appears alongside it. The winner is always chosen by unique open rate subject to a minimum-opens floor. Test duration is a stage-level constant rather than per-test configuration, so it is not a field here — read the actual window from `startedAt` / `endsAt`.
abTest.statusstring, one of: draft | scheduled | running | finalizing | completed | cancelledalways present——Where the test is in its lifecycle, and the ONLY way to tell live figures from frozen ones: while `running` the per-variant numbers keep moving as recipients open, and at `completed` they are frozen to the snapshot taken when the test ended. In practice only `running`, `finalizing`, `completed` and `cancelled` are reachable here — a `draft` or `scheduled` test belongs to an email that has not sent, which 404s before the test is read.
abTest.winnerstring, one of: A | B | tie | noneabsent when the test has not finalized — `omitempty` upstream, so a running or finalizing test carries no key at all. That ABSENCE is a distinct third state: `none` means finalized with no variant clearing the minimum-opens floor, `tie` means finalized with equal open rates, and an absent key means not decided yet——The winning variant. `none` and `tie` are real recorded outcomes, not errors and not placeholders. A cancelled test is finished yet never picks one, so absence covers both "not decided yet" and "cancelled".
abTest.startedAtstring (rfc3339)absent when the test never started——When the test window opened.
abTest.endsAtstring (rfc3339)absent when the test never started——When the test window closes. The duration is a stage-level constant rather than a per-test setting — 24 hours on production, shorter on staging — so read the window from these timestamps instead of assuming a length.
abTest.finalizedAtstring (rfc3339)absent when the test has not completed——When the winner was recorded and the per-variant figures were frozen.
abTest.cancelledAtstring (rfc3339)absent when the test was not cancelled——When an in-flight test was terminated.
abTest.sampleSizeintegeralways presentwhole count—The planned test audience TOTAL across both arms — not per variant — fixed when the test began and split between them, variant A taking the odd recipient when it does not divide evenly.
abTest.remainderSizeintegeralways presentwhole count—The held-back audience that ships after the test concludes, carrying the winning subject — or, when the winner is `none`, split across the two variant subjects (A taking the odd recipient) rather than falling back to the original. Usually `0` when the audience was small enough to split with nothing held back, though it is derived as recorded-recipients minus sample, so an audience that grew after the test began can report a non-zero size on a test that held nothing back. Once it ships it is why the top-level counts exceed the per-variant ones; before that they do not.
abTest.sentCountintegeralways presentwhole count—How many recipients have been sent to so far, across the sample and any remainder already shipped. Stays LIVE even after the test completes, because the remainder keeps sending.
abTest.pendingCountintegeralways presentwhole count—How many recipients are still waiting to be sent to, on a running test. On a CANCELLED test nothing further ships, so this is usually `0` — cancelling rewrites the campaign total down to the locked sample. It stays POSITIVE when the cancel landed before that sample finished sending, or when assigned recipients were suppressed at send: those people were dropped, not queued.
abTest.variantsarray of objectalways present——One entry per variant, A then B. Always two entries for a real test — never empty, and never a single entry.
abTest.variants[].namestring, one of: A | Balways present——The variant label. Two variants is the only shape the product supports today.
abTest.variants[].subjectstringalways present——The subject line this variant tested, as a recipient saw it: stored as editor HTML and relayed only after the same parse-and-render pair the send pipeline uses, so markup is stripped, entities are decoded, and personalization tokens resolve to their authored default rather than arriving as `{{ subscriber.firstName | … }}` template source. The campaign-level `subject` goes through the identical helper, so the two never disagree in form. An unset one stays `""` rather than falling back to the campaign subject — in practice only variant B, since A is seeded from the campaign subject when the test is created.
abTest.variants[].sentintegeralways presentwhole count—Sends for this variant’s share of the sample only.
abTest.variants[].uniqueOpensintegeralways presentwhole count—Distinct subscribers who opened this variant.
abTest.variants[].uniqueClicksintegeralways presentwhole count—Distinct subscribers who clicked this variant.
abTest.variants[].openRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—This variant’s unique opens divided by its DELIVERIES (not its sends); `0` when nothing was delivered. The delivered count is not a returned field, so this rate cannot be re-derived from `sent`. This is the metric the winner is chosen on.
abTest.variants[].clickRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—This variant’s unique clicks divided by its deliveries; `0` when nothing was delivered.
abTest.variants[].isWinnerbooleanalways present——Whether this variant won. False on BOTH entries when the winner is `none` or `tie`, and on both while the test is still undecided.

structuredContent: An object with the single key `stats`, carrying the same payload one level deeper — so every field above is at `stats.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response. It is a single-email read; one email plus at most two variants is far inside the response cap.

Dates

No date inputs: the result is not scoped by a date window. A single campaign has a single send, so there is no window to scope — for a date-scoped view across emails use `rank_emails` or `get_email_trends`.

Response variants

get_email_totals

Returns account-wide email performance for a window or for all time, as counts of emails and sends plus four rates.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
fromstringno—1–64 characters—
tostringno—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. Exactly six fields, always all six. Opens and clicks appear only as rates: there is no unique-open or unique-click COUNT on this tool — use `rank_emails` for per-email counts. Both the windowed and lifetime query paths fill the same six fields, so the shape does not change with the window.

FieldTypePresenceUnitsDerived in the connectorMeaning
totalEmailsintegeralways presentwhole count—How many emails were sent in the window (or over the account’s lifetime).
totalSendsintegeralways presentwhole count—How many individual sends those emails produced.
deliveryRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Deliveries divided by sends.
openRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique opens divided by DELIVERIES, not by sends. The underlying unique-open count is not returned.
clickRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique clicks divided by deliveries. The underlying unique-click count is not returned.
unsubRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unsubscribes divided by deliveries.

structuredContent: An object with the single key `totals`, carrying the same payload one level deeper — so every field above is at `totals.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

Accepts `from` and `to` as either a bare date (`2026-09-01`) or a full RFC-3339 timestamp. `from` is coerced to start-of-day and `to` to end-of-day, in the window the backend compares; an omitted `to` defaults to today. `from` must be on or before `to`, and the resolved span must not exceed 365 days — both are client-side validation errors, not upstream ones. Omitting `from` selects the lifetime path and sends no window upstream.

Response variants

get_me

Returns the connected Flodesk account’s own user record — who the connector is acting as.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. The seven-key account record. Every key is unconditionally present because the handler writes a literal map rather than a struct with `omitempty`, so nothing here can be absent or null. The endpoint deliberately carries NO plan, billing or feature-access field, so a plan tier cannot be read through the connector.

FieldTypePresenceUnitsDerived in the connectorMeaning
idstringalways present——The Flodesk user id of the account.
emailstringalways present——The account owner’s login email address.
fullNamestringalways present——The owner’s name as a single string, `""` when unset. There is no `firstName` or `lastName` on this record — those belong to subscriber shapes.
timezonestringalways present——The account’s configured timezone.
countryCodestringalways present——The account’s country code.
emailVerifiedbooleanalways present——Whether the owner’s own address is verified. A boolean, not a timestamp.
createdAtstring (rfc3339)always present——When the account was created; the zero time `"0001-01-01T00:00:00Z"` if it was never set.

structuredContent: An object with the single key `user`, carrying the same payload one level deeper — so every field above is at `user.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

get_segment_overlap

Answers how many subscribers two or more static segments share, alongside each segment’s own size — the aggregate intersection, without moving a single subscriber record through the model.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
segment_idsarray of stringyes—2–10 items; each item 1–64 characters; matching ^[A-Za-z0-9_-]+$Two to ten segment ids, as returned by `list_segments`. No duplicates. Static segments only.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. Cross-segment membership: the shared-subscriber count plus each named segment’s own size. The only read in the catalog that relates segments to each other rather than reporting on them side by side.

FieldTypePresenceUnitsDerived in the connectorMeaning
overlapCountintegeralways presentwhole count of subscribers, never a rate—How many active subscribers belong to EVERY segment named in the request — the N-way intersection, not a pairwise or any-of figure. `0` is a real answer meaning the segments share nobody, and is distinguishable from the error cases, which return no counts at all. Because it is counted over the same predicate as each `totalActives`, it can never exceed the smallest of them.
segmentsarray of objectalways present——One row per id in the request, 2–10 of them. Every named segment gets a row: an empty segment appears with `totalActives: 0` rather than being dropped. Aggregate only by construction — the connector projects these four keys explicitly, so no subscriber id, email or other per-person field can reach the model through this tool.
segments[].idstringalways present——The segment id, echoed from the request — the same value `list_segments` returns and `inspect_segment` takes as `segment_id`.
segments[].namestringalways present——The segment’s name, resolved server-side, so the overlap can be reported without a second `list_segments` call.
segments[].segmentTypestringalways present——The RAW segment type, exactly as stored — never `mapSegmentType`’d, so a `pre_built` segment would stay `pre_built` rather than collapsing into `dynamic`. In practice only `static` is ever observed here: a non-static id is rejected upstream with a 4xx before any counting happens, so it never reaches a row. It is a plain string rather than a closed enum so a segment type the backend adds later surfaces as-is.
segments[].totalActivesintegeralways presentwhole count of subscribers, never a rate—That segment's OWN current size — the denominator for reading `overlapCount` as a share. Counted live on the read replica at request time, over the same `deleted IS FALSE AND status = 'confirmed'` predicate as `overlapCount` — not read from the stored aggregate that `list_segments` and `inspect_segment` report. Two consequences worth planning around: the number can differ slightly from what those tools say about the same segment, and it is never `null`, because there is no aggregate that might be missing.

structuredContent: An object with the single key `overlap`, carrying the same payload one level deeper — so every field above is at `overlap.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

get_send_time_insights

Groups email engagement by weekday, part of day, or device class, to show when and where an audience engages.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
group_bystringyes—one of: dayOfWeek | timeOfDay | deviceType—
periodstringno—one of: last7Days | last30Days | last90Days—
fromstringno—1–64 characters—
tostringno—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. Exactly ONE top-level key, whichever `group_by` asked for; the other two are omitted. The label key is `groupName` (not `group`) and the share key is `percentage` (not `share`), and the three groupings do not share a field set.

FieldTypePresenceUnitsDerived in the connectorMeaning
dayOfWeekarray of objectconditional — `group_by: "dayOfWeek"`. The other two top-level keys are then ABSENT entirely (Go `omitempty`), not null and not `[]`——Always the full seven-row roster, Monday through Sunday, zero-filled where there is no data. This is the only grouping that carries rate fields and the only one without `percentage`.
dayOfWeek[].groupNamestring, one of: Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sundayalways present——The weekday this row aggregates.
dayOfWeek[].totalSendsintegeralways presentwhole count—Sends made on that weekday.
dayOfWeek[].openRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique opens divided by deliveries for that weekday.
dayOfWeek[].clickRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique clicks divided by deliveries for that weekday.
timeOfDayarray of objectconditional — `group_by: "timeOfDay"`; absent otherwise——Always the full four-row roster, zero-filled where there is no data. Carries no rate fields.
timeOfDay[].groupNamestring, one of: Morning | Afternoon | Evening | Nightalways present——The part of day this row aggregates.
timeOfDay[].totalUniqueOpensintegeralways presentwhole count—Distinct subscribers who opened during that part of day.
timeOfDay[].percentagenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—This row’s share of all unique opens in the window — a bare part/whole ratio with no ×100.
deviceTypearray of objectconditional — `group_by: "deviceType"`; absent otherwise——Always the full two-row roster, zero-filled where there is no data. Carries no rate fields.
deviceType[].groupNamestring, one of: Desktop | Mobilealways present——The device class this row aggregates.
deviceType[].totalUniqueClicksintegeralways presentwhole count—Distinct subscribers who clicked from that device class.
deviceType[].percentagenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—This row’s share of all unique clicks in the window — a bare part/whole ratio with no ×100.

structuredContent: An object with the single key `insights`, carrying the same payload one level deeper — so every field above is at `insights.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

Accepts `period` (`last7Days`, `last30Days` or `last90Days`) XOR `from`/`to` — supplying both is a client-side validation error rather than one silently winning. Accepts `from` and `to` as either a bare date (`2026-09-01`) or a full RFC-3339 timestamp. `from` is coerced to start-of-day and `to` to end-of-day, in the window the backend compares; an omitted `to` defaults to today. `from` must be on or before `to`, and the resolved span must not exceed 365 days — both are client-side validation errors, not upstream ones. Omitting `from` selects the lifetime path and sends no window upstream.

Response variants

get_subscriber

Returns one subscriber’s profile by email or id, including status, source, segment memberships and last engagement.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
emailstringno—1–320 characters—
idstringno—1–320 characters—
include_custom_fieldsbooleanno———
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. One subscriber profile, relayed verbatim: nothing on this tool is renamed, dropped or computed by the connector.

FieldTypePresenceUnitsDerived in the connectorMeaning
idstringalways present——The subscriber id.
emailstringalways present——The subscriber’s email address.
firstNamestringalways present——First name, `""` when unset. The key is always present.
lastNamestringalways present——Last name, `""` when unset. The key is always present.
statusstring, one of: active | unconfirmed | unsubscribed | bounced | complained | cleaned | archivedalways present——Subscription state. `active` is the backend’s `confirmed`, rewritten upstream; every other value passes through verbatim.
createdAtstring (rfc3339)always present——When the subscriber was added.
sourcestringalways present——A plain-language label for how the subscriber joined — `Signup form`, `Added manually`, `Imported from a file`, `Integration` or `Checkout`, and the raw backend value for a source type Flodesk has not labelled yet. NOTE the divergence: here it is the bare label, whereas `list_subscribers` appends `": <form name>"` for form rows.
segmentsarray of objectalways present——Segment memberships, `[]` when none — never null. A segment deleted between the two reads is silently skipped rather than failing the profile. There is no segment id here, so reaching `inspect_segment` needs a `list_segments` name lookup.
segments[].namestringalways present——The segment’s name.
segments[].typestring, one of: static | dynamicalways present——Only `static` or `dynamic` on the subscriber routes — a `pre_built` segment is reported as `dynamic` here, unlike `list_segments` and `inspect_segment`, which return the raw type.
lastOpenedAtstring (rfc3339)always present; null when the subscriber has never opened an email——When they last opened an email, from a separate single-row activity query. The key is ALWAYS present and carries JSON `null` for "never" — deliberately not the epoch, and deliberately not absent (contrast `list_email_recipients`, where the same-named key is omitted).
lastClickedAtstring (rfc3339)always present; null when the subscriber has never clicked a link——When they last clicked a link, from a separate single-row activity query. Always present; `null` means never.
customFieldsobject with dynamic keysconditional — `include_custom_fields: true` was passed. With the flag off or absent the key is omitted entirely; with it on and no values stored, it is `{}` rather than null——Stored custom-field values as a dynamic-keyed object. Values are whatever was stored — the upstream type is arbitrary JSON, not necessarily a string.
customFields.<key>any JSON value (string, number, boolean, null, array or object)one entry per key present in the payload——the account’s custom-field machine keys — the same keys `update_subscriber` sets values by

structuredContent: An object with the single key `subscriber`, carrying the same payload one level deeper — so every field above is at `subscriber.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

get_subscriber_totals

Returns the current subscriber-list snapshot and, when a window is given, how much it changed against the preceding window of the same length.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
periodstringno—one of: last7Days | last30Days—
fromstringno—1–64 characters—
tostringno—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. Two upstream responses joined under two connector-chosen keys. Neither response is renamed, projected or recomputed.

FieldTypePresenceUnitsDerived in the connectorMeaning
overallobjectalways present—`overall` is a key name fd-mcp-server gives the first upstream response.Exactly three keys, and NEVER window-scoped: the endpoint behind them takes no query parameters at all, so these stay current/trailing-fixed even when `period` or `from`/`to` is supplied. There is no `totalUnsub` here — unsubscribes appear only inside `change`.
overall.totalActivesintegeralways presentwhole count—Subscribers with `active` status right now.
overall.totalNew30dintegeralways presentwhole count—Subscribers added in the trailing 30 days.
overall.totalNew7dintegeralways presentwhole count—Subscribers added in the trailing 7 days.
changeobjectconditional — the caller supplied `period` OR `from`; with neither, the second upstream call is skipped and the key is absent entirely—`change` is a key name fd-mcp-server gives the second upstream response.The windowed comparison. Its three keys are `totalActives`, `totalNew` and `totalUnsub` — not `added`/`removed` — and each is an OBJECT, not a plain integer.
change.totalActivesobjectalways present——Active subscribers, with its change against the preceding window. An OBJECT, not a plain integer.
change.totalActives.valuenumberalways presenta raw count, float64-encoded — not a rate—Active subscribers in the window.
change.totalActives.changenumberabsent when the prior equal-length window had none — a pointer with `omitempty`, so the key is ABSENT, never null and never 0a fractional delta, so 0.25 means +25% — not a count and not percentage points—The relative change versus the immediately preceding window of the same length: `(current − previous) / previous`.
change.totalNewobjectalways present——Subscribers added, with its change against the preceding window. An OBJECT, not a plain integer.
change.totalNew.valuenumberalways presenta raw count, float64-encoded — not a rate—Subscribers added in the window.
change.totalNew.changenumberabsent when the prior equal-length window had none — a pointer with `omitempty`, so the key is ABSENT, never null and never 0a fractional delta, so 0.25 means +25% — not a count and not percentage points—The relative change versus the immediately preceding window of the same length: `(current − previous) / previous`.
change.totalUnsubobjectalways present——Subscribers who unsubscribed, with its change against the preceding window. An OBJECT, not a plain integer.
change.totalUnsub.valuenumberalways presenta raw count, float64-encoded — not a rate—Subscribers who unsubscribed in the window.
change.totalUnsub.changenumberabsent when the prior equal-length window had none — a pointer with `omitempty`, so the key is ABSENT, never null and never 0a fractional delta, so 0.25 means +25% — not a count and not percentage points—The relative change versus the immediately preceding window of the same length: `(current − previous) / previous`.

structuredContent: An object with the single key `summary`, carrying the same payload one level deeper — so every field above is at `summary.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response. Neither upstream endpoint is paginated.

Dates

Accepts `period` (`last7Days` or `last30Days`) XOR `from`/`to` — supplying both is a client-side validation error. Accepts `from` and `to` as either a bare date (`2026-09-01`) or a full RFC-3339 timestamp. `from` is coerced to start-of-day and `to` to end-of-day, in the window the backend compares; an omitted `to` defaults to today. `from` must be on or before `to`, and the resolved span must not exceed 365 days — both are client-side validation errors, not upstream ones. Omitting `from` selects the lifetime path and sends no window upstream. Whichever is used scopes ONLY the `change` half.

Response variants

inspect_segment

Returns one segment’s type, membership rule and stored size — what the segment IS, rather than who is in it.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
segment_idstringyes—1–64 charactersThe id of one segment, as returned by `list_segments`.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. One segment’s definition and size, in the emitted key order `id`, `name`, `type`, optional `filterExpression`, `totalSubscribers`, `activeSubscribers`. The payload is scoped to definition plus size: `color` and `createdAt` are deliberately not relayed, and an explicit projection keeps any future upstream field out until it is reviewed.

FieldTypePresenceUnitsDerived in the connectorMeaning
idstringalways present——The segment id.
namestringalways present——The segment’s name.
typestring, one of: static | dynamic | pre_builtalways present—renamed from upstream `segmentType`.The RAW segment type — `pre_built` is never collapsed into `dynamic` here. A type Flodesk adds later surfaces as-is rather than failing the read.
filterExpressionstring (filter-expression)absent when the segment is `static` — upstream guarantees a nil rule there, so the key is dropped rather than emitted as null——The membership rule verbatim in Flodesk’s expression syntax, byte for byte as stored: not parsed, normalized or translated into the structured `filter` those other tools take. It deliberately does not reuse the name `filter` for that reason. Present for `dynamic` and `pre_built`, never null.
totalSubscribersintegeralways present; null when there is no usable stored aggregate — a custom `dynamic` segment (always, however many subscribers its rule matches), a `pre_built` one before its first materialization, or a `static` one whose aggregate row was invalidatedwhole count, never a rate—All members, as of the last stored aggregate — never recomputed for the call. `null` means the size is unknown, `0` means the tally really is zero. This is the count `list_segments` drops; `inspect_segment` keeps both.
activeSubscribersintegeralways present; null when there is no usable stored aggregate — a custom `dynamic` segment (always, however many subscribers its rule matches), a `pre_built` one before its first materialization, or a `static` one whose aggregate row was invalidatedwhole count, never a raterenamed from upstream `totalActiveSubscribers`.Members with `active` status, as of the same stored aggregate. Named to match `list_segments`, and backed by the same aggregate query, so the two segment reads cannot disagree about a segment.

structuredContent: An object with the single key `segment`, carrying the same payload one level deeper — so every field above is at `segment.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response. It is a single-resource read; use `list_subscribers` to page the members themselves.

Dates

No date inputs: the result is not scoped by a date window. The counts come from stored membership, so there is no window to request and no live recount — `preview_segment_count` is the live-count path.

Response variants

list_email_recipients

Lists the individual subscribers who opened or clicked one email, optionally narrowed to one clicked link.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
email_idstringyes—1–200 charactersId of an email, from `rank_emails`.
engagementstringyes—one of: opened | clicked—
linkstringno—1–2048 charactersA url; valid only with engagement: 'clicked'.
limitintegerno—1 to 100Default 25 when omitted.
pageintegerno—1 to 10000000001-based; default 1 when omitted.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. The upstream envelope relayed verbatim. Three facets share it — `opened`, `clicked` without a `link`, and `clicked` with a `link` — and they differ in which keys are present, never in what a key means.

FieldTypePresenceUnitsDerived in the connectorMeaning
totalintegeralways presentwhole count—How many recipients match this facet in total, independent of `page` and `limit` and identical on every page. On an empty page beyond the first the count is recovered by re-issuing the same query for one row, so it stays true rather than collapsing to 0.
pageintegeralways present——The page actually served, after the server floors an invalid or non-positive page to 1 and clamps above 1,000,000,000.
hasMorebooleanalways present——True exactly when `total > page × limit`, computed on this endpoint rather than read off a pagination struct. False on the last full page and on any page past the end. This is the stop condition.
recipientsarray of objectalways present——The page of recipients, `[]` when none — never null. Which engagement keys a row carries is decided by the facet, so the row shape differs between `opened` and `clicked`.
recipients[].idstringalways present——The subscriber id.
recipients[].emailstringalways present——Their email address.
recipients[].firstNamestringalways present——First name, `""` when unset OR when the subscriber row no longer exists (deleted since the send).
recipients[].lastNamestringalways present——Last name, `""` when unset or unresolvable.
recipients[].statusstring, one of: active | unconfirmed | unsubscribed | bounced | complained | cleaned | archived | always present——Current subscription state, or `""` when the subscriber row no longer exists (deleted since the send). The lookup runs without a status filter, so recipients who later unsubscribed still resolve with their real status.
recipients[].lastOpenedAtstring (rfc3339)absent when `engagement` is `clicked` (the clicked facets never set it), or the subscriber has no open recorded — the key carries `omitempty`, so it is ABSENT, never null——The subscriber’s most recent open of this email. The key name is `lastOpenedAt`, and it appears only on the `opened` facet.
recipients[].opensintegerabsent when `engagement` is `clicked`, or the count is 0 — `omitempty` on a non-pointer int drops a zero, so a missing key means zero, not unknownwhole count—How many times this subscriber opened the email. The key name is `opens`.
recipients[].lastClickedAtstring (rfc3339)absent when `engagement` is `opened` (that facet never sets it), or the subscriber has no click recorded — the key carries `omitempty`, so it is ABSENT, never null——The subscriber’s most recent click. The key name is `lastClickedAt`, and it appears only on the two `clicked` facets.
recipients[].clicksintegerabsent when `engagement` is `opened`, or the count is 0 — a missing key means zero, not unknownwhole count—How many times this subscriber clicked. The key name is `clicks`.
linksarray of objectconditional — `engagement: "clicked"` with NO `link`, and the email has at least one clicked link. Absent on the `opened` facet, absent on a `link` drill-in, and absent when the email has no clicked links (an empty slice plus `omitempty`)——Up to 50 clicked links for the WHOLE email, ordered by url ascending and then truncated — NOT by click count, so when `totalLinks` exceeds this length the omitted links may include the most-clicked ones. It describes the email, not the page, and repeats unchanged on every recipient page.
links[].urlstringalways present——The NORMALIZED link url (dynamic tracking params collapsed to a placeholder) — the same normalization the `link` drill-in matches against, so a url copied from here works verbatim as the `link` input.
links[].uniqueClicksintegeralways presentwhole count—Distinct subscribers who clicked this link.
links[].totalClicksintegeralways presentwhole count—All clicks on this link, repeats included.
totalLinksintegerabsent when it would be 0 — which includes the `opened` facet and the `link` drill-in, neither of which sets it at allwhole count—The true count of distinct clicked links, independent of the 50-entry cap, so a caller can tell when `links` is only a top slice.
linkstringconditional — the `link` drill-in facet only — no other facet emits this key——The requested link url, echoed back in its normalized form.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Paged by `page` (1-based, maximum 1,000,000,000) and `limit` (1–100; the backend defaults to 25 and clamps above 100). `total`, `page` and `hasMore` are BODY fields, so page until `hasMore` is false. `links` and `totalLinks` do NOT page with the rows: they are fetched at page 1 with a 50-row cap and describe the whole email, so they repeat identically on every page.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

list_segments

Lists the account’s segments with their ids, types and active-member counts, one page at a time — the whole roster is reachable, and this is the way to turn a segment name into an id.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
pageintegerno—1 to 10000000001-based; default 1 when omitted.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. One page of the account’s segment roster, including segments `rank_segments` cannot see: this route reads mcp-api’s own segment list rather than the analytics view that inner-joins stored aggregates, so a never-materialized dynamic segment appears here with a `null` count instead of vanishing.

FieldTypePresenceUnitsDerived in the connectorMeaning
segmentsarray of objectalways present—renamed from the upstream `data` envelope.The segments on this page, newest-created first, `[]` on an account with none and on any page past the end. Upstream’s `totalSubscribers` is deliberately dropped from each row, as is any future upstream field — `inspect_segment` is where both counts are returned.
segments[].idstringalways present——The segment id — the value `inspect_segment` takes as `segment_id`.
segments[].namestringalways present——The segment’s name.
segments[].typestring, one of: static | dynamic | pre_builtalways present—renamed from upstream `segmentType`.The RAW segment type: `pre_built` stays `pre_built` here, unlike `get_subscriber`’s memberships, which collapse it into `dynamic`.
segments[].activeSubscribersintegeralways present; null when there is no usable stored aggregate — every custom `dynamic` segment, a `pre_built` one awaiting its first materialization, or a `static` one whose aggregate row was invalidatedwhole count, never a raterenamed from upstream `totalActives`, relayed with no `?? 0`.Members with `active` status, as of the last stored aggregate. `null` means the size is NOT COMPUTED and `0` means the tally really is zero — the value is relayed uncoerced precisely so those two cannot be confused.
totalintegerconditional — the upstream sent it — which it always does today (a non-pointer int), though the handler spreads it conditionallywhole count—How many segments the account has in total, independent of how many rows came back and identical on every page. Still correct on a page past the end.
pageintegeralways present—relayed from upstream `page`, falling back to the requested page only while an older mcp-api omits the field.The 1-based page actually served, echoing the `page` input (1 when it was omitted). The server floors an invalid or non-positive page to 1 and clamps above 1,000,000,000; the advertised maximum equals that cap so the echo matches the request.
hasMorebooleanalways present—relayed from upstream `hasMore`, falling back to `total > offset + rows` only while an older mcp-api omits the field.True exactly when segments beyond this page exist — so it is false on the last page and on any page past the end. This is the stop condition for walking the roster; do not infer one from the row count.

structuredContent: Identical to the text payload — the same object is sent as `structuredContent`, so the key paths above are the paths a client reads on both channels.

Pagination

Paged by `page` alone (1-based, maximum 1,000,000,000, default 1). Page SIZE is not an input: every call requests the server’s cap of 100, so the offset is `(page - 1) * 100`. `total`, `page` and `hasMore` are BODY fields, so the stop condition is explicit: page until `hasMore` is false. Ordering is fixed newest-created-first upstream and is not a caller input, which is what keeps a just-created segment on page 1. Paging reads the live roster, so a segment created or deleted between two page requests can move an entry across a page boundary.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

list_subscribers

Lists the subscribers matching a structured filter, one page at a time, with the true total for the whole match.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
filterobjectno——Required unless `emails` or `ids` is given — a filter with 1–10 conditions; an unfiltered audience is not allowed.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
emailsarray of stringno—1–100 items; each item 1–320 characters1–100 subscriber email addresses to return, matched case-insensitively; duplicates collapse. Not combinable with `ids`.
idsarray of stringno—1–100 items; each item 1–320 characters; matching ^[A-Za-z0-9-]+$1–100 subscriber ids (the `id` a row or `get_subscriber` returns) to return; duplicates collapse. Not combinable with `emails`.
signup_formstringno—1–200 charactersA signup-form name, not an id.
limitintegerno—1 to 100Default 25 when omitted.
pageintegerno—1 to 10000000001-based; default 1 when omitted.
sortstringno—one of: lastActiveAt | createdAt | openedEmails_l30d | openedEmails_l90d | openedEmails_l365d | openedEmails | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l365d | clickedEmails—
directionstringno—one of: asc | descOmitted sorts descending (backend default 'desc').
include_segmentsbooleanno——Omitted or false leaves `segments` off every row.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. The upstream list envelope, relayed verbatim — nothing renamed, dropped or computed by the connector.

FieldTypePresenceUnitsDerived in the connectorMeaning
totalintegeralways presentwhole count—How many subscribers match the filter (and, when given, are in the `emails` / `ids` lookup set) in total — its own count over the same filter with no offset or limit term, so it is INDEPENDENT of `page` and `limit`, identical on every page, and still correct on a page past the end.
pageintegeralways present——The page actually served. The server floors an invalid or non-positive page to 1 and clamps above 1,000,000,000; the advertised maximum equals that cap so the echo matches the request.
hasMorebooleanalways present——True exactly when `page` is below `ceil(total / limit)` — so it is false on the last full page (a total divisible by the limit) and false on any page past the end. This is the stop condition; do not infer one from the row count.
subscribersarray of objectalways present——The page of rows, `[]` when none — never null. Rows are deliberately narrower than `get_subscriber`’s profile to keep per-row PII down: a row never carries `customFields` at all, and carries `segments` only when `include_segments: true` asked for it.
subscribers[].idstringalways present——The subscriber id.
subscribers[].emailstringalways present——Their email address.
subscribers[].firstNamestringalways present——First name, `""` when unset.
subscribers[].lastNamestringalways present——Last name, `""` when unset.
subscribers[].statusstring, one of: active | unconfirmed | unsubscribed | bounced | complained | cleaned | archivedalways present——Subscription state; `active` is the backend’s `confirmed`.
subscribers[].sourcestringalways present——How the subscriber joined, WITH the signup-form name appended for form rows — `"Signup form: Newsletter"` — resolved by one bulk form lookup over the page. `get_subscriber` returns the same conceptual field as the bare label with no form name.
subscribers[].createdAtstring (rfc3339)always present——When the subscriber was added.
subscribers[].lastOpenedAtstring (rfc3339)always present; null when the subscriber has never opened an email——Last open. The key is ALWAYS present and `null` means never — the same key is ABSENT rather than null on `list_email_recipients`.
subscribers[].lastClickedAtstring (rfc3339)always present; null when the subscriber has never clicked a link——Last click. Always present; `null` means never.
subscribers[].segmentsarray of objectconditional — `include_segments: true` was passed. With the flag off or absent the key is omitted entirely; with it on and the subscriber in no segment, it is `[]` rather than null——The subscriber’s segment memberships, resolved by ONE bulk lookup over the whole page rather than per row — the lookup’s own row order is never observed, since it is collapsed into an id→{name,type} map and fanned back out. Entries therefore follow the subscriber’s stored `segment_ids` order, minus any id the lookup did not return; that order is not promised and not stable — do not read position as rank or recency. A segment deleted between the page scan and the lookup is one such missing id: it is skipped rather than rendered nameless, so a row can carry fewer entries than the account’s own records suggest. Same `{ name, type }` shape as `get_subscriber`, and the same absence of a segment id.
subscribers[].segments[].namestringalways present——The segment’s name.
subscribers[].segments[].typestring, one of: static | dynamicalways present——Only `static` or `dynamic` on the subscriber routes — a `pre_built` segment is reported as `dynamic` here, unlike `list_segments` and `inspect_segment`, which return the raw type.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Paged by `page` (1-based, maximum 1,000,000,000) and `limit` (1–100; the backend defaults to 25 when omitted and clamps anything above 100). Unlike the ranged analytics tools, `total`, `page` and `hasMore` are BODY fields here, so the stop condition is explicit: page until `hasMore` is false. Rows are ordered by `sort` (default `lastActiveAt`) and `direction` (default `desc`), and paging reads the live list — a concurrent write can move a row across a page boundary, so a row can be seen twice or missed between pages.

Dates

No date-window inputs. Date-valued filter conditions take an absolute RFC-3339 timestamp (`2026-05-08T00:00:00Z`) or a relative `"-N days|weeks|months"` string; a bare `2026-05-08` is rejected.

Response variants

preview_bulk_change

Reports how many subscribers a bulk archive, unarchive, segment addition, segment removal or CSV export would affect, with a sample and a short-lived confirmation token.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
actionstringyes—one of: archive | unarchive | remove_from_segment | add_to_segment | exportThe bulk change being previewed. The token is only valid for this action.
filterobjectno——The group to act on. Required for every action except `export`, which takes this or `segment_id`.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
segment_idstringno—1–64 charactersFor `add_to_segment`, the segment being added to. For `export`, the saved segment to export — pass this or `filter`, never both. Not accepted for the other actions.
signup_formstringno—1–200 charactersA signup-form name, not an id.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. The read half of the two-step bulk change: it states the cohort truthfully and hands back a short-lived token, and it changes no subscriber. One upstream list call answers both halves, because the upstream total is the true total independent of the 10-row limit.

FieldTypePresenceUnitsDerived in the connectorMeaning
affectedintegeralways presentwhole countrenamed from the upstream `total`.How many subscribers the change would touch — the TRUE total for the filter, independent of the 10-row sample. This is the number a person is being asked to approve.
samplearray of objectalways present——At most 10 example rows, newest subscribers first (by when they joined the account), `[]` on the zero-cohort path — never null. It is a SAMPLE for a human check, not the cohort: only `affected` states the size. Each row is projected to three keys; the upstream row’s `firstName`, `lastName`, `source`, `createdAt`, `lastOpenedAt` and `lastClickedAt` are dropped, as are the envelope’s `page` and `hasMore`.
sample[].idstringalways present——The subscriber id.
sample[].emailstringalways present——Their email address.
sample[].statusstring, one of: active | unconfirmed | unsubscribed | bounced | complained | cleaned | archivedalways present——Their current status, so an already-archived cohort is visible before a write.
actionstring, one of: archive | unarchive | remove_from_segment | add_to_segment | exportalways present—an echo of the input.The change that was previewed, one of the five bulk actions. The token is only valid for this action, so spending it on a tool that performs a different one is refused.
filterobjectconditional — the caller supplied a `filter`. Absent on a segment-scoped export, which names its cohort with `segment_id` instead and echoes that; never null—an echo of the parsed input.The filter that was counted, echoed as the structured input. The bulk tool must be given this filter UNCHANGED: the token is bound to a hash of the compiled form, so any edit is refused. On a segment-scoped export the resolved filterlang string is deliberately NOT echoed here — it is an internal binding, and handing it back under a key the filter tools accept would invite it to be fed in as a condition object.
filter.matchstring, one of: all | anyalways present——Whether every condition or any condition had to match. Carries the schema default `all` when the caller omitted it, and is emitted before `conditions`.
filter.conditionsarray of objectalways present——The 1–10 conditions, echoed in the order they were supplied.
filter.conditions[].fieldstringalways present——The subscriber attribute or engagement metric compared.
filter.conditions[].operatorstring, one of: gt | ge | lt | le | eq | ne | containsalways present——The comparison operator, echoed as supplied. The legal set narrows by field: `gt`/`ge`/`lt`/`le` for dates and rates, plus `eq` for counts; `eq`/`ne` for `status` and `sourceType`; `contains` for `segmentIds` membership. Note `ge`/`le`, not `gte`/`lte`.
filter.conditions[].valuestring or numberalways present——The compared value: a string for dates, status and sourceType; a number for rates and engagement counts.
segment_idstringconditional — the caller supplied a `segment_id`, which only `add_to_segment` and `export` accept. Absent for `archive`, `unarchive` and `remove_from_segment`, which reject it outright, and never null—an echo of the input.The segment id echoed back, meaning DIFFERENT things per action. For `add_to_segment` it is the DESTINATION the matching subscribers would be added to, echoed EXACTLY as supplied including its case, while the value the token binds is lower-cased — so an id typed one way here and the other way at the write is still the same segment, and spending that token on any other segment is refused — it appears alongside `filter`, which names the group being added. For `export` it is the SELECTOR: the saved segment whose members would be exported, and no `filter` is echoed at all. The other three actions take their group from `filter` alone and accept no `segment_id`.
signup_formstringconditional — the caller supplied a `signup_form`. Absent otherwise, never null—an echo of the input.The signup-form NAME the cohort was scoped to, echoed at the top level. It must be passed to the bulk tool exactly as it appears here — dropping it would widen the cohort beyond what the token authorized, and the token binding refuses that.
confirmation_tokenstring (base64url)conditional — `affected` is above 0 AND the token mint succeeded. It is absent on the zero-cohort payload; a mint failure returns an error instead, so a preview is never returned without a token—minted in fd-mcp-server from 32 random bytes and stored in Redis. It has no upstream origin.The single-use token that authorizes the change, to be passed after a person confirms to whichever tool performs the previewed `action` — `bulk_archive_subscribers`, `bulk_unarchive_subscribers`, `bulk_add_subscribers_to_segment`, `bulk_remove_subscribers_from_segment` or `export_subscribers`. An opaque 43-character string — never parse it. It is bound to this account, this action and this exact selector, and it is claimed atomically, so a replay is refused rather than repeated.
expires_in_secondsintegerconditional — `affected` is above 0 AND the token mint succeeded. It is absent on the zero-cohort payload; a mint failure returns an error instead, so a preview is never returned without a tokenseconds; always 120a fixed lifetime the connector sets, applied as the Redis key expiry and returned unchanged; never an upstream value.How long the token stays valid from now. Always 120 — a fixed constant, and the same value used as the Redis key expiry, so the advertised number and the real expiry cannot diverge. After it elapses, preview again.
messagestringconditional — `affected` is 0 — mutually exclusive with the token pair by construction, since a zero cohort is never issued a token—composed in fd-mcp-server, with the wording forked so an export is not told there is “nothing to change”.A sentence explaining that nothing matches the selector, so no token was issued, and inviting a different one. The wording forks on the action: every action but `export` is told there is nothing to CHANGE, and `export` that there is nothing to EXPORT — an export alters no subscriber, so the generic wording would send a reader looking for the wrong fix.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated, and deliberately so: there is no page input, the sample is capped at 10 rows client-side and upstream, and `affected` carries the full size. The upstream envelope’s `page` and `hasMore` are dropped, so this tool cannot be walked to enumerate the cohort — use `list_subscribers` with the same filter for that.

Dates

No date inputs: the result is not scoped by a date window. Date-valued filter conditions take an absolute RFC-3339 timestamp or a relative `"-N days|weeks|months"` string.

Response variants

preview_segment_count

Counts the subscribers a filter matches, without creating anything and without returning any subscriber data.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
filterobjectyes——Required — a filter with 1–10 conditions; an unfiltered audience or count is not allowed.
filter.matchstringno"all"one of: all | any—
filter.conditionsarray of objectyes—1–10 itemsAn array of 1–10 condition objects. Always pass an array, even when there is only one condition.
filter.conditions[].fieldstringyes—one of: createdAt | lastActiveAt | lastUnsubscribedAt | openRate | clickRate | openedEmails_l7d | openedEmails_l30d | openedEmails_l90d | openedEmails_l180d | openedEmails_l365d | clickedEmails_l7d | clickedEmails_l30d | clickedEmails_l90d | clickedEmails_l180d | clickedEmails_l365d | deliveredEmails_l7d | deliveredEmails_l30d | deliveredEmails_l90d | deliveredEmails_l180d | deliveredEmails_l365d | openedEmails | clickedEmails | deliveredEmails | status | sourceType | segmentIdsThe subscriber attribute or engagement metric to compare.
filter.conditions[].operatorstringyes—one of: gt | ge | lt | le | eq | ne | containsComparison operator. Valid choices depend on field: dates and rates use gt, ge, lt, or le only (never eq or ne); counts use gt, ge, lt, le, or eq; status and sourceType use eq or ne only.
filter.conditions[].valuestring | numberyes——Comparison value. Use a string for dates, status, and sourceType; use a number for rates and engagement counts. Dates must be an absolute RFC-3339 timestamp (e.g. "2026-05-08T00:00:00Z") or a relative "-N days|weeks|months" (e.g. "-90 days") — a bare "2026-05-08" is rejected. Rates are decimals between 0 and 1 (e.g. 0.27 = 27%). Engagement counts are non-negative integers.
signup_formstringno—1–200 charactersA signup-form name, not an id.
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. The one tool that builds its own payload rather than relaying an upstream one: the upstream response carries a single `count` field, and every other key is an echo of the input or minted MCP-side.

FieldTypePresenceUnitsDerived in the connectorMeaning
countintegeralways presentwhole count, never a rate—How many subscribers match the filter right now. It is the same number `list_subscribers` reports as `total` — both come from the same count query — so a preview and a listing cannot disagree. `0` is a normal success, not an error.
filterobjectalways present—built in fd-mcp-server from the parsed input; `match` carries its schema default when the caller omitted it.The filter that was counted, echoed back as the STRUCTURED input rather than the compiled expression sent upstream — so it can be confirmed and handed unchanged to `create_segment`.
filter.matchstring, one of: all | anyalways present——Whether every condition or any condition had to match. Carries the schema default `all` when the caller omitted it, and is emitted before `conditions`.
filter.conditionsarray of objectalways present——The 1–10 conditions, echoed in the order they were supplied.
filter.conditions[].fieldstringalways present——The subscriber attribute or engagement metric compared.
filter.conditions[].operatorstring, one of: gt | ge | lt | le | eq | ne | containsalways present——The comparison operator, echoed as supplied. The legal set narrows by field: `gt`/`ge`/`lt`/`le` for dates and rates, plus `eq` for counts; `eq`/`ne` for `status` and `sourceType`; `contains` for `segmentIds` membership. Note `ge`/`le`, not `gte`/`lte`.
filter.conditions[].valuestring or numberalways present——The compared value: a string for dates, status and sourceType; a number for rates and engagement counts.
signup_formstringconditional — the caller supplied a `signup_form`. Absent otherwise — never null, and never nested inside `filter`—an echo of the input.The signup-form NAME the count was scoped to, echoed at the top level. Its presence is what tells a reader the count was form-scoped rather than account-wide.
confirmation_tokenstringconditional — `count` is above zero AND the token store was reachable. Absent on a zero count, and absent — with a `message` saying so — when the mint failed—minted MCP-side into Redis by `mintToken` ; the upstream count endpoint knows nothing about it.A single-use token authorizing `create_segment` for exactly this cohort: the compiled filter plus the normalized `signup_form`, bound to the calling account. It binds no destination, because the segment it authorizes does not exist yet. It is valid ONLY for creating a segment — every bulk write refuses it as a wrong-action token — and spending it twice is refused as already-used.
expires_in_secondsintegerconditional — `confirmation_token` is presentseconds—How long the token stays redeemable — a fixed connector-side constant, not an upstream value.
messagestringconditional — no token accompanies the count — either `count` is 0, or the mint failed. Absent on the ordinary tokened success——Why no token is present, and what that means for creating a segment. On a zero count: nothing matched, so there is nothing to authorize. On a mint failure: the count is still accurate and the number is the answer, but a segment cannot be created from it until the token store recovers. The count is deliberately NOT failed in that case — this is a Live read tool whose primary job is the number, and the fail-closed guarantee lives at `create_segment`s redeem instead.

structuredContent: An object with the single key `result`, carrying the same payload one level deeper — so every field above is at `result.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response. The tool deliberately sends no `limit`, `page` or `sort`: it is the count-only sibling of `list_subscribers` and returns no rows at all.

Dates

No date inputs: the result is not scoped by a date window. Date-valued filter conditions take an absolute RFC-3339 timestamp or a relative `"-N days|weeks|months"` string.

Response variants

rank_emails

Ranks sent emails by one performance metric, returning per-email counts, rates and subject lines.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
order_bystringno—one of: sentAt | totalSends | totalUniqueOpens | openRate | totalUniqueClicks | clickRate | unsubRate—
sortstringno—one of: asc | desc—
pageintegerno—1 to ∞—
per_pageintegerno—1 to 20—
searchstringno—1–200 characters—
segment_idstringno—1–64 characters—
fromstringno—1–64 characters—
tostringno—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A single-key envelope. There is no `total`, `page`, `perPage` or `hasMore` anywhere in this response.

FieldTypePresenceUnitsDerived in the connectorMeaning
dataarray of objectalways present——The ranked page, `[]` when nothing matched — never null and never absent. It is the ONLY key in this payload.
data[].idstringalways present——The email id — this is the value `list_email_recipients` takes as `email_id`. A resend row carries its PARENT’s id, so the same id can appear more than once in one page.
data[].namestringalways present——The internal email name; a resend row reads `"<name> - Resend"`.
data[].sentAtstring (rfc3339)always present——When the email was sent.
data[].totalSendsintegeralways presentwhole count—Individual sends for this email.
data[].totalUniqueOpensintegeralways presentwhole count—Distinct subscribers who opened it.
data[].openRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique opens divided by deliveries.
data[].totalUniqueClicksintegeralways presentwhole count—Distinct subscribers who clicked it.
data[].clickRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique clicks divided by deliveries.
data[].unsubRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unsubscribes divided by deliveries. Also a valid `order_by` value.
data[].subjectstringalways present—the VALUE is chosen in fd-mcp-server: the analytics row’s subject when non-empty, otherwise the subject from a second upstream campaign read joined on `id`, otherwise `""`.The subject line as a recipient saw it, possibly `""` when it could not be resolved on either source. The key is always present. Stored as editor HTML and relayed only after the same parse-and-render pair the send pipeline uses, so markup is stripped, entities are decoded, and a personalization token resolves to its authored default rather than arriving as `{{ subscriber.firstName | … }}` template source — the same helper `get_email_stats` uses. The fallback source (the separate campaign lookup used when the analytics row carries no subject) is relayed raw, so in that narrow case the two tools could in principle differ in form.
data[].abTestobjectalways present; null when the campaign ran no split test — the common case, since most campaigns are not tests——The compact A/B summary, carried so split tests are visible among the rows on THIS page rather than by probing each campaign. It is not filterable or sortable — there is no A/B input parameter — and rows beyond the requested page carry no signal at all. The row’s own counts and rates are BLENDED across both variants plus the remainder send, not the winning variant alone. A RESEND row is the exception: it carries the original’s id, subject and this block while its counts are the resend’s own, so its rates describe no split test at all and its subject is the original’s, identical to the parent row on the same page. Per-variant figures live on `get_email_stats` only.
data[].abTest.statusstring, one of: draft | scheduled | running | finalizing | completed | cancelledalways present——Where the split test is in its lifecycle. Only `running`, `finalizing`, `completed` and `cancelled` are reachable here — this list ranks sent campaigns, and a `draft` or `scheduled` test belongs to one that has not sent.
data[].abTest.winnerstring, one of: A | B | tie | noneabsent when the test has not picked a winner — either still undecided, or CANCELLED, which is finalized yet never picks one. `omitempty` upstream, so no key at all. That ABSENCE is a third state, distinct from `none` (finalized, but no variant cleared the minimum-opens floor) and from `tie`——The winning variant once the test finalizes. `none` is a real outcome, not an error.
data[].templateSourcestringabsent when the campaign recorded no template source at all. All three template keys are omitempty, so a campaign duplicated from another — which records the empty source — carries none of them——Where the email’s design came from — a gallery template, a private gallery template, a template the user saved, a scratch build, Canva, or Studio. Resolved by the exact ids on this ranked page, so it carries no recency cap, unlike the subject fallback above.
data[].templateIdstringabsent when the campaign referenced no template — built from scratch, or duplicated from another campaign——The stable identifier of the template this email was built on. It is what groups a ranked page by design without reading a single email body; `get_email_content` returns the same identifier for one email.
data[].templateNamestringabsent when the referenced template record carries no name — a template the user saved themselves has none, and neither does a gallery template that has since been deleted——The template’s display name. Absence is a fact about the stored record, never a failed lookup: the identifier is still returned and is sufficient to group by design.
data[].recipientScopeobjectalways present; null when the campaign has no stored audience filter — the scope is UNKNOWN. That is a different answer from a scope with empty segment lists, which means nobody was addressed; treating unknown as empty is what makes a cross-campaign average wrong——Who this email was sent to. Comparing campaigns with different recipient scopes is not like-for-like: a curated segment and an all-subscribers send routinely differ several-fold in open rate. A resend row is reported under its parent’s id and therefore carries the ORIGINAL send’s scope — an upper bound on who the resend reached.
data[].recipientScope.allSubscribersbooleanalways present——True when the email was addressed to every subscriber. It can be true alongside a non-empty `excludedSegments` — "everyone except the cold list" is one scope, not two.
data[].recipientScope.includedSegmentsarray of objectalways present——The segments the email was addressed to, `[]` when none — never null. Empty with `allSubscribers` true is the normal all-subscribers shape: the system segment is the marker, not a listed entry.
data[].recipientScope.includedSegments[].idstringalways present——The segment id, kept even when the segment no longer exists.
data[].recipientScope.includedSegments[].namestringalways present; null when the segment was deleted after the send and can no longer be resolved. The id above survives regardless, so the reported audience is never silently narrower than the one that received the email——The segment’s CURRENT name — no historical name is stored, so a segment renamed since the send shows its new name.
data[].recipientScope.includedSegments[].unresolvedbooleanabsent when the segment resolved normally——Set to `true` alongside a null `name` when the segment was deleted after the send. Pairs with `name: null`; check this rather than inferring from the name.
data[].recipientScope.excludedSegmentsarray of objectalways present——The segments held back from the send, `[]` when none — never null. Present so the reported audience matches who actually received the email rather than only who it was aimed at.
data[].recipientScope.excludedSegments[].idstringalways present——The segment id, kept even when the segment no longer exists.
data[].recipientScope.excludedSegments[].namestringalways present; null when the segment was deleted after the send and can no longer be resolved. The id above survives regardless, so the reported audience is never silently narrower than the one that received the email——The segment’s CURRENT name — no historical name is stored, so a segment renamed since the send shows its new name.
data[].recipientScope.excludedSegments[].unresolvedbooleanabsent when the segment resolved normally——Set to `true` alongside a null `name` when the segment was deleted after the send. Pairs with `name: null`; check this rather than inferring from the name.
data[].recipientScope.addedSubscribersintegeralways presentwhole count—How many individual subscribers were added on top of the segments. Their identities are deliberately NOT returned.
data[].recipientScope.removedSubscribersintegeralways presentwhole count—How many individual subscribers were removed from the send. Identities are not returned.
data[].recipientScope.omittedSegmentsintegerabsent when every segment fitted in the two lists above — the overwhelmingly common casewhole count—How many segments were dropped from `includedSegments`/`excludedSegments` to keep the response within the tool result size limit. At most five segments are listed per campaign, exclusions kept first. When present, the lists are a prefix of the audience rather than all of it.

structuredContent: An object with the single key `results`, carrying the same payload one level deeper — so every field above is at `results.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Paged by `page` (1-based) and `per_page` (1–20; no advertised default — the backend uses 10 and silently resets anything outside 1–20 to 10). The upstream writes `X-Total`, `X-Total-Pages`, `X-Page` and `X-Per-Page` as HTTP response headers and the connector’s client returns only the parsed body, so the response carries NO `total`, `page`, `perPage` or `hasMore` field. A short or empty `data` array is the only end-of-collection signal, and an empty page cannot be told apart from a page past the end. When `search` is set, the caller's `page` and `per_page` are IGNORED: the connector requests the newest 100 rows on the chosen metric with no page and intersects them client-side with the search matches. So a search result can exceed `per_page`, can be `[]` even when 100 rows ranked, and silently omits matching emails ranked below position 100 — and the search side is separately capped at the backend's default 20 campaigns.

Dates

Accepts `from` and `to` as either a bare date (`2026-09-01`) or a full RFC-3339 timestamp. `from` is coerced to start-of-day and `to` to end-of-day, in the window the backend compares; an omitted `to` defaults to today. `from` must be on or before `to`, and the resolved span must not exceed 365 days — both are client-side validation errors, not upstream ones. Omitting `from` selects the lifetime path and sends no window upstream.

Response variants

rank_forms

Ranks signup forms by traffic or opt-in performance, alongside the account-wide form summary.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
order_bystringno—one of: totalVisitors | totalOptIns | optInRate—
sortstringno—one of: asc | desc—
pageintegerno—1 to ∞—
per_pageintegerno—1 to 20—
fromstringno—1–64 characters—
tostringno—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. Two upstream responses joined: the summary under a connector-chosen `overall` key, and the ranked list’s own `data` key alongside it. Nothing is renamed, dropped or recomputed.

FieldTypePresenceUnitsDerived in the connectorMeaning
overallobjectalways present—`overall` is a key name fd-mcp-server gives the first upstream response; the second response’s own keys are spread as siblings rather than nested.The account-wide summary, always complete — it does not paginate with `data`, so a page past the end still returns full totals.
overall.totalVisitorsintegeralways presentwhole count—Visitors across all forms, window-scoped when `from` is given.
overall.totalOptInsintegeralways presentwhole count—Opt-ins across all forms, window-scoped when `from` is given.
overall.optInRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Opt-ins divided by visitors across all forms.
overall.hasDataBeforeNov22booleanalways present——An upstream flag for whether the account has form data before the November 2022 boundary the analytics service tracks (Go field `DataBeforeNov`). It is relayed to the caller and mentioned by no tool description; read it as a completeness hint about older figures, not as a metric.
dataarray of objectalways present——The ranked page of forms, `[]` when empty — never null. This is the only paged half of the payload.
data[].idstringalways present——The form id.
data[].namestringalways present——The form’s name.
data[].totalVisitorsintegeralways presentwhole count—Visitors to this form.
data[].totalOptInsintegeralways presentwhole count—Opt-ins from this form.
data[].optInRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—This form’s opt-ins divided by its visitors.
data[].createdAtstring (rfc3339)always present——When the form was created.

structuredContent: An object with the single key `results`, carrying the same payload one level deeper — so every field above is at `results.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Paged by `page` (1-based) and `per_page` (1–20; no advertised default — the backend uses 10 and silently resets anything outside 1–20 to 10). The upstream writes `X-Total`, `X-Total-Pages`, `X-Page` and `X-Per-Page` as HTTP response headers and the connector’s client returns only the parsed body, so the response carries NO `total`, `page`, `perPage` or `hasMore` field. A short or empty `data` array is the only end-of-collection signal, and an empty page cannot be told apart from a page past the end. Only `data` is paged — `overall` is always the complete account-wide summary.

Dates

Accepts `from` and `to` as either a bare date (`2026-09-01`) or a full RFC-3339 timestamp. `from` is coerced to start-of-day and `to` to end-of-day, in the window the backend compares; an omitted `to` defaults to today. `from` must be on or before `to`, and the resolved span must not exceed 365 days — both are client-side validation errors, not upstream ones. Omitting `from` selects the lifetime path and sends no window upstream.

Response variants

rank_segments

Ranks segments by size or by email engagement, to show which parts of an audience respond best.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
order_bystringno—one of: totalActives | openRate | clickRate—
sortstringno—one of: asc | desc—
pageintegerno—1 to ∞—
per_pageintegerno—1 to 20—
fromstringno—1–64 characters—
tostringno—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. The upstream envelope relayed verbatim: no field renamed, none dropped, nothing derived, no unit converted.

FieldTypePresenceUnitsDerived in the connectorMeaning
dataarray of objectalways present——The ranked page, `[]` when empty — never null. It is the only key in this payload. INCOMPLETENESS TO KNOW ABOUT: on the lifetime path (no `from`) the underlying view inner-joins the stored segment aggregate, so a segment with no aggregate — every custom dynamic segment, since those are never materialized — is missing ENTIRELY rather than reported with zeros. `list_segments` is the complete roster.
data[].segmentIdstringalways present——The segment id. The key is `segmentId`, not `id` — this tool renames nothing, unlike `list_segments`, which projects to `id`/`name`.
data[].segmentNamestringalways present——The segment’s name; `""` when the stored name is null.
data[].segmentColorstringalways present——The segment’s colour as stored; `""` when null. Relayed verbatim and mentioned by no tool description.
data[].totalActivesintegeralways presentwhole count—Members with `active` status — the segment’s CURRENT size, which is not scoped by the date window even on a windowed call. Only the rates respond to the window.
data[].openRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique opens divided by deliveries for this segment.
data[].clickRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique clicks divided by deliveries for this segment.
data[].percentagenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—This segment’s share of the account’s total active subscribers — `0` when the account has none. Mentioned by no tool description.

structuredContent: An object with the single key `results`, carrying the same payload one level deeper — so every field above is at `results.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Paged by `page` (1-based) and `per_page` (1–20; no advertised default — the backend uses 10 and silently resets anything outside 1–20 to 10). The upstream writes `X-Total`, `X-Total-Pages`, `X-Page` and `X-Per-Page` as HTTP response headers and the connector’s client returns only the parsed body, so the response carries NO `total`, `page`, `perPage` or `hasMore` field. A short or empty `data` array is the only end-of-collection signal, and an empty page cannot be told apart from a page past the end.

Dates

Accepts `from` and `to` as either a bare date (`2026-09-01`) or a full RFC-3339 timestamp. `from` is coerced to start-of-day and `to` to end-of-day, in the window the backend compares; an omitted `to` defaults to today. `from` must be on or before `to`, and the resolved span must not exceed 365 days — both are client-side validation errors, not upstream ones. Omitting `from` selects the lifetime path and sends no window upstream.

Response variants

rank_workflows

Ranks automations by entries, completions or engagement; with a `workflow_id`, returns that workflow’s step graph and per-step counts instead.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
order_bystringno—one of: totalEntries | totalCompletions | openRate | clickRate | unsubRate—
sortstringno—one of: asc | desc—
pageintegerno—1 to ∞—
per_pageintegerno—1 to 20—
workflow_idstringno—1–64 characters—
fromstringno—1–64 characters—
tostringno—1–64 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. TWO DISJOINT PAYLOADS under one tool, selected by `workflow_id`. Without it: a single `data` array of ranked workflows. With it: one workflow object with fifteen top-level keys and no `data` at all. The two share only the meanings of `id`, `name` and `status` — every other key belongs to exactly one mode.

FieldTypePresenceUnitsDerived in the connectorMeaning
dataarray of objectconditional — `workflow_id` was omitted — list mode. Absent in detail mode——The ranked page of workflows, `[]` when empty — never null. Ten keys per row: nine relayed plus `completionRate`.
data[].idstringalways present——The workflow id — pass it back as `workflow_id` for the per-step breakdown.
data[].namestringalways present——The workflow’s name.
data[].statusstring, one of: active | paused | draftalways present——The workflow’s state.
data[].totalEntriesintegeralways presentwhole count—Subscribers who entered the workflow; window-scoped when `from` is given.
data[].totalCompletionsintegeralways presentwhole count—Subscribers who completed it; window-scoped when `from` is given.
data[].totalActivesintegeralways presentwhole count—Subscribers currently inside the workflow. The key is `totalActives` — not `totalActiveSubscribers`, and not the checkout rows’ `totalActiveSubscriptions`.
data[].openRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique opens divided by deliveries for this workflow’s emails.
data[].clickRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unique clicks divided by deliveries.
data[].unsubRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)—Unsubscribes divided by deliveries.
data[].completionRatenumberalways presentdecimals between 0 and 1 (e.g., 0.27 = 27%)computed per row in fd-mcp-server as `totalCompletions ÷ totalEntries`. Not an upstream field.Completions divided by entries, or exactly `0` when there were no entries — never null, never NaN. It cannot be used as `order_by`, because the upstream cannot sort by a value computed here.
idstringconditional — `workflow_id` was supplied — detail mode. Absent in list mode——The workflow id.
versionintegerconditional — `workflow_id` was supplied — detail mode. Absent in list mode——The stored version number of this workflow.
namestringconditional — `workflow_id` was supplied — detail mode. Absent in list mode——The workflow’s name.
statusstring, one of: active | paused | draftconditional — `workflow_id` was supplied — detail mode. Absent in list mode——The workflow’s state.
nodesarray of objectconditional — `workflow_id` was supplied — detail mode. Absent in list mode——The workflow’s steps, `[]` for a stepless draft. Per-step values are INTEGER COUNTS, never rates: derive step conversion (`totalCompletedTimes ÷ totalEnteredTimes`) and the drop-off between adjacent steps yourself. Unreachable steps are pruned before the response is built, so the graph can be smaller than the editor shows.
nodes[].uidstringalways present——A legacy duplicate of `id`; the upstream struct says to use `id` instead. Relayed as-is.
nodes[].idstringalways present——The step id.
nodes[].namestringalways present——The step name from the workflow step-name roster (for example `sendEmail`). Together with `type` it identifies what the step does.
nodes[].typestring, one of: trigger | action | delay | condition | confirmedSubscriberCondition | multipleBranch | joinalways present——The step type, which also decides how its two count fields are computed.
nodes[].dataobject with dynamic keysalways present——The step’s raw configuration payload, relayed verbatim. Its keys and value types are decided by the step type, and it is `{}` rather than null when the step stores nothing.
nodes[].data.<key>any JSON value (string, number, boolean, null, array or object)one entry per key present in the payload——step-specific configuration keys — an email-send step carries `emailId`, which is the id `list_email_recipients` accepts
nodes[].isRequiredbooleanalways present——Whether the step cannot be removed from the workflow.
nodes[].orderintegeralways present——The step’s ordinal position in the stored graph.
nodes[].totalSubscribersintegeralways presentwhole count—Subscribers sitting AT this step right now — a point-in-time snapshot, not a cumulative figure. `0` when the step has no stats row.
nodes[].totalEnteredTimesintegeralways presentwhole count—How many times subscribers reached this step. Computed upstream per step type: a `delay` step counts starts, every other type counts finishes — which is what keeps the funnel monotonic.
nodes[].totalCompletedTimesintegeralways presentwhole count—How many times subscribers finished this step, with force-stops subtracted so it can never exceed `totalEnteredTimes`.
nodes[].totalUniqueOpensintegerabsent when the step is not an email-send step. It is present and `0` on an email step with no engagement, so the KEY’s presence — not its value — is how an email step is identified in the payloadwhole count—Distinct subscribers who opened the email this step sends.
nodes[].totalUniqueClicksintegerabsent when the step is not an email-send step, exactly as for `totalUniqueOpens`whole count—Distinct subscribers who clicked in the email this step sends.
edgesarray of objectconditional — `workflow_id` was supplied — detail mode. Absent in list mode——The transitions between steps; `[]` when there are none.
edges[].sourcestringalways present——The step id this edge leaves.
edges[].targetstringalways present——The step id it enters.
edges[].isFixedbooleanalways present——Whether the transition is structural rather than user-editable.
edges[].valueany JSON value (string, number, boolean, null, array or object)absent when the transition stores no value——The transition’s stored condition value, relayed verbatim. Its shape is decided by the source step type.
edges[].branchIdstringabsent when the transition belongs to no branch (an empty id)——Which branch of a multi-branch step this transition belongs to.
configobjectconditional — `workflow_id` was supplied — detail mode. Absent in list mode——Entry and repeat configuration for the workflow.
config.repeatAllowedbooleanalways present——Whether a subscriber may re-enter the workflow.
config.repeatConfigobjectalways present——Always present even when repeats are off: the `omitempty` on it upstream has no effect on a Go struct VALUE, so expect a zeroed object rather than a missing key.
config.repeatConfig.durationintegeralways present——How long before re-entry is allowed, in units of `unit`.
config.repeatConfig.unitstring, one of: hour | day | always present——The unit `duration` is measured in, or `""` when repeats were never configured on this workflow.
config.excludedSegmentIdsarray of stringabsent when no ids are stored for this workflow. `excludedSegmentIds,omitempty` on the upstream Go slice drops both nil and empty, and the normalizer that would turn nil into `[]` is not called on this read path — so the key is absent or a NON-EMPTY array, and `[]` never appears——Segments whose members are excluded from entering.
config.excludedWorkflowIdsarray of stringabsent when no ids are stored for this workflow, exactly as for `excludedSegmentIds` — absent or non-empty, never `[]`——Workflows whose members are excluded from entering.
isAbandonedCartWorkflowbooleanconditional — `workflow_id` was supplied — detail mode. Absent in list mode——True when any step is triggered by an abandoned checkout.
shouldExpandTriggersbooleanconditional — `workflow_id` was supplied — detail mode. Absent in list mode——An editor hint relayed from the stored workflow.
shouldMigrateConditionsbooleanconditional — `workflow_id` was supplied — detail mode. Absent in list mode——An editor hint relayed from the stored workflow.
sanitizedbooleanconditional — `workflow_id` was supplied — detail mode. Absent in list mode——An upstream flag on the stored workflow, relayed as-is.
tagstringconditional — `workflow_id` was supplied — detail mode. Absent in list mode——The workflow’s tag, `""` when unset.
sharedTemplateIdstringconditional — `workflow_id` was supplied — detail mode. Absent in list mode——The shared template this workflow came from, `""` when none.
createdAtstring (rfc3339)conditional — `workflow_id` was supplied — detail mode. Absent in list mode——When the workflow was created.
updatedAtstring (rfc3339)conditional — `workflow_id` was supplied — detail mode. Absent in list mode——When the workflow was last changed.

structuredContent: An object with the single key `results`, carrying the same payload one level deeper — so every field above is at `results.<path>` when a client reads `structuredContent` instead of the text.

Pagination

Paged by `page` (1-based) and `per_page` (1–20; no advertised default — the backend uses 10 and silently resets anything outside 1–20 to 10). The upstream writes `X-Total`, `X-Total-Pages`, `X-Page` and `X-Per-Page` as HTTP response headers and the connector’s client returns only the parsed body, so the response carries NO `total`, `page`, `perPage` or `hasMore` field. A short or empty `data` array is the only end-of-collection signal, and an empty page cannot be told apart from a page past the end. Detail mode takes no paging input and returns the whole graph in one response.

Dates

List mode: Accepts `from` and `to` as either a bare date (`2026-09-01`) or a full RFC-3339 timestamp. `from` is coerced to start-of-day and `to` to end-of-day, in the window the backend compares; an omitted `to` defaults to today. `from` must be on or before `to`, and the resolved span must not exceed 365 days — both are client-side validation errors, not upstream ones. Omitting `from` selects the lifetime path and sends no window upstream. Detail mode accepts NO date window — passing `from` or `to` with `workflow_id` is a client-side validation error rather than a silently ignored input, and the per-step counts are always lifetime.

Response variants

remove_subscriber_from_segment

Removes one subscriber from one static segment, idempotently.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
emailstringno—1–320 characters—
idstringno—1–320 characters—
segment_idstringyes—1–320 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A four-key receipt, relayed verbatim. It removes a membership, not the subscriber: nothing here reports a deletion.

FieldTypePresenceUnitsDerived in the connectorMeaning
removedbooleanalways present——True when this call removed the membership, false when the subscriber was not in the segment. BOTH are successes; `false` means "was not a member", never "failed". Note the key name: it is `removed` here and `added` on the add tool — the boolean’s name is the only difference between the two receipts.
subscriberobjectalways present——Just the two identifying keys — no status, names or memberships.
subscriber.idstringalways present——The subscriber id.
subscriber.emailstringalways present——Their email address.
segmentobjectalways present——The segment written to, so the name can be confirmed against the id that was sent.
segment.idstringalways present——The segment id.
segment.namestringalways present——The segment’s name.
messagestringalways present——A sentence stating the outcome — `Removed <email> from segment "<name>".`, or `<email> is not in segment "<name>".` when `removed` is false. Composed UPSTREAM, not by the connector.

structuredContent: Identical to the text payload — the same object is sent as `structuredContent`, so the key paths above are the paths a client reads on both channels.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

unsubscribe_subscriber

Unsubscribes one subscriber, and reports whether the change actually applied or their status already prevented it.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
emailstringno—1–320 characters—
idstringno—1–320 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. A three-key receipt, relayed verbatim. It changes status only: no segment membership, name or custom field is touched, and nothing is deleted.

FieldTypePresenceUnitsDerived in the connectorMeaning
changedbooleanalways present——True when this call unsubscribed them, false when their status did not allow it. BOTH are successes: the backend checks for an error BEFORE reading this flag, precisely so a genuine failure can never arrive as a clean `changed: false`.
subscriberobjectalways present——Three keys: this is the only one of the four simple write receipts whose nested subscriber carries a `status`, and it carries no `segment` key at all.
subscriber.idstringalways present——The subscriber id.
subscriber.emailstringalways present——Their email address.
subscriber.statusstring, one of: active | unconfirmed | unsubscribed | bounced | complained | cleaned | archivedalways present——Their ACTUAL current status — `unsubscribed` after a successful change, or the real state that blocked it (`unconfirmed`, `bounced`, `complained`, `cleaned`). It is the backend’s echo and is never composed on the connector side.
messagestringalways present——A per-status sentence, not boilerplate. On success it states the change is irreversible from the connector and that the subscriber must opt back in through Flodesk. On `changed: false` it explains the blocking status specifically — an `unconfirmed` subscriber is described as still pending double opt-in and NOT as an opt-out, a `bounced` one as hard-bounced, a `complained` one as having reported spam, and any other suppressed state as no longer receiving email. Composed UPSTREAM.

structuredContent: Identical to the text payload — the same object is sent as `structuredContent`, so the key paths above are the paths a client reads on both channels.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants

update_subscriber

Updates one subscriber’s name or custom fields and returns the stored values after the write.

Parameters

ParameterTypeRequiredAdvertised defaultBoundsDescription
emailstringno—1–320 characters—
idstringno—1–320 characters—
first_namestringno—0–100 characters—
last_namestringno—0–100 characters—
custom_fieldsobject with dynamic keys, values stringno—keys 1–100 characters; each value 0–1000 characters—
original_promptstringno—0–8192 charactersProvide a one-sentence summary of the user's task intent for this turn (example: 'wants top-performing email campaigns this month'). Used solely for product analytics; does not change the response. Do not include the full conversation, prior turns, the system prompt, or content unrelated to this tool's task. Omit if the user did not provide an explicit task.

Returned fields

Text payload: object, always present. TWO keys only, and NO boolean flag — there is no `changed`, `updated` or `added` here, unlike the other three simple writes. Every accepted call performs a write, so there is no idempotent no-op variant to distinguish.

FieldTypePresenceUnitsDerived in the connectorMeaning
subscriberobjectalways present——The updated profile: the create-path shape plus `customFields`. It still omits `createdAt`, `source` and the activity timestamps that `get_subscriber` carries.
subscriber.idstringalways present——The subscriber id.
subscriber.emailstringalways present——Their email address.
subscriber.firstNamestringalways present——First name AFTER the update, `""` when unset or just cleared. Read it back to confirm what was applied.
subscriber.lastNamestringalways present——Last name after the update, `""` when unset or just cleared.
subscriber.statusstring, one of: active | unconfirmed | unsubscribed | bounced | complained | cleaned | archivedalways present——Subscription state. This tool never changes it — use `unsubscribe_subscriber` for that.
subscriber.segmentsarray of objectalways present——Memberships after the update, `[]` when none. The re-read comes from the writer, so a dynamic-segment membership shifted BY this edit is already reflected rather than replica-lagged.
subscriber.segments[].namestringalways present——The segment’s name.
subscriber.segments[].typestring, one of: static | dynamicalways present——Only `static` or `dynamic` on the subscriber routes — a `pre_built` segment is reported as `dynamic` here, unlike `list_segments` and `inspect_segment`, which return the raw type.
subscriber.customFieldsobject with dynamic keysalways present——All stored custom-field values after the update — ALWAYS an object, `{}` when empty rather than null, so applied values can be confirmed without a second `get_subscriber`.
subscriber.customFields.<key>any JSON value (string, number, boolean, null, array or object)one entry per key present in the payload——the account’s custom-field machine keys — the same keys this tool sets values by, and the same keys `get_subscriber` surfaces
messagestringalways present——A sentence naming each changed field and its new value — `Updated <email>: first_name → Jane; custom field 'company' → Acme.` A field set to `""` reads as `first_name cleared`, and custom-field keys are sorted so the sentence is deterministic. Composed UPSTREAM.

structuredContent: Identical to the text payload — the same object is sent as `structuredContent`, so the key paths above are the paths a client reads on both channels.

Pagination

Not paginated: no page or per-page input, and no `total`, `page`, `perPage` or `hasMore` field in the response.

Dates

No date inputs: the result is not scoped by a date window.

Response variants