{"server":{"name":"roninconv","title":"RoninConv","version":"1.7.0","description":"First-party click tracking: zero-redirect lander serving, flow routing, postback attribution, breakdown reporting, Meta cost sync. Cloudflare Workers + Neon."},"protocol":"MCP (Model Context Protocol), Streamable HTTP transport, stateless","endpoint":"/mcp","production_endpoint":"https://mcp.roninconv.com/mcp","auth":"Two ways in, same workspace pinning either way: (1) API key — Authorization: Bearer <rk_…>, minted in Settings → API keys; (2) OAuth 2.1 — clients like claude.ai connectors discover /.well-known/oauth-protected-resource/mcp, register dynamically, and the user signs in + picks ONE workspace and access level on the consent page. Session tokens also work with an X-Workspace header.","count":157,"categories":{"platform":"Platform — identity, docs, bulk ops, raw access","campaigns":"Campaigns — tracked links + routing flows","landers":"Landers — hosted landing pages","offers":"Offers & networks — monetized destinations","domains":"Domains — edge serving hostnames","traffic-sources":"Traffic sources — S2S conversion feedback","pixels":"Pixels — CAPI targets + configured Meta browser pairing","reporting":"Reporting — breakdowns, conversions, raw events","webhooks":"Webhooks & postbacks — event in/out","settings":"Settings — bot filtering","meta":"Meta cost — ad spend integration","admin":"Admin — workspaces, members, keys, audit"},"tools":[{"name":"whoami","title":"Who am I","category":"platform","scope":null,"readOnly":true,"destructive":false,"covers":["GET /me"],"description":"Identify the current actor: API key or user, active workspace, role, exact permission strings, saved user timezone and user-owned reporting table_preferences. Keys receive UTC and an empty preference map. Call this first to learn your workspace.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"update_user_preferences","title":"Update my preferences","category":"platform","scope":"preferences:write","readOnly":false,"destructive":false,"covers":["PATCH /me/preferences"],"description":"Save the current user’s IANA timezone and/or reporting table layouts across sessions, devices and workspaces. At least one field is required. Each supplied table ID replaces that table’s complete layout; other tables are preserved by an atomic merge. Only report supports custom formulas; their IDs must start custom_ and be unique. Every table supports optional labels: at most 128 stable column IDs (1–100 characters) mapped to trimmed nonempty display labels of at most 120 characters. Omit an alias from the replacement layout to restore its default; omitted labels means {}. Labels never change IDs, metrics, formulas, filters or CSV schema. Optional sort saves {key,direction:\"asc\"|\"desc\"} per table; null or omitted is unsorted. Preserve sort when replacing other layout fields and preserve layout fields when sorting. Optional freezeFirstColumn persists horizontal freezing of the first visible data column and any selection checkbox column; omitted means false. Preserve it on sort/layout edits. Requires a user session or OAuth user with preferences:write; API keys are rejected. Existing users default to UTC and {}. Timezone affects report day grouping and dashboard display; storage/wire timestamps remain UTC. Returns authoritative persisted {timezone,table_preferences}.","inputSchema":{"type":"object","properties":{"timezone":{"type":"string","description":"UTC or IANA Area/Location, e.g. America/New_York; no offsets or abbreviations","maxLength":100,"pattern":"^(?:UTC|[A-Za-z][A-Za-z0-9_+-]*(?:/[A-Za-z0-9_+-]+)+)$"},"table_preferences":{"type":"object","properties":{"dashboard-campaigns":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"dashboard-offers":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"campaigns":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"landers":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"offers":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"networks":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"network-offers":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"report":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":128,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"User-owned report formula definitions"}},"required":["order","hidden","widths"],"additionalProperties":false},"events":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"events-view":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"events-click":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"conversions":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"postbacks":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false},"delivery-errors":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Column IDs in left-to-right order; no restriction on identifier characters"},"hidden":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100},"maxItems":128,"description":"Hidden column IDs"},"widths":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"number","minimum":32,"maximum":1600},"description":"Column ID to width in CSS pixels"},"labels":{"type":"object","maxProperties":128,"propertyNames":{"type":"string","minLength":1,"maxLength":100},"additionalProperties":{"type":"string","minLength":1,"maxLength":200,"pattern":"^\\S(?:[\\s\\S]*\\S)?$"},"description":"Optional display-only aliases keyed by stable column ID (1–100 characters); at most 128 trimmed nonempty labels, each at most 200 characters. Omit an alias to restore its default; omitted labels means {}. IDs, formulas, filters and CSV schema stay unchanged."},"sort":{"anyOf":[{"type":"null"},{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}},"required":["key","direction"],"additionalProperties":false}],"description":"Saved per-table sort. Null or omitted means incoming/API order; preserve on layout edits."},"freezeFirstColumn":{"type":"boolean","description":"Freeze the first visible data column plus selection checkbox column during horizontal scrolling. Defaults false; preserve when replacing sorting or other fields."},"custom":{"type":"array","maxItems":0,"uniqueItems":true,"items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^custom_","description":"Stable ID starting custom_, unique within custom; e.g. custom_<uuid>"},"name":{"type":"string","description":"Column label","maxLength":200},"expr":{"type":"string","description":"Report formula expression","maxLength":1000}},"required":["id","name","expr"],"additionalProperties":false},"description":"Only the report table supports custom definitions; omit or send [] here"}},"required":["order","hidden","widths"],"additionalProperties":false}},"additionalProperties":false,"maxProperties":128,"description":"Table ID to complete replacement layout. Only supplied table IDs are merged; all other saved tables and the timezone remain unchanged."}},"additionalProperties":false,"anyOf":[{"required":["timezone"]},{"required":["table_preferences"]}]}},{"name":"get_docs","title":"Get developer docs","category":"platform","scope":null,"readOnly":true,"destructive":false,"covers":[],"description":"Return the full RoninConv AI/developer guide (markdown): concepts, object model, typical agent workflows, REST reference, postback contract, maintenance rules. Also served publicly at /docs (HTML), /docs/ai (markdown) and /llms.txt on the API host.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"api_request","title":"Raw API request","category":"platform","scope":null,"readOnly":false,"destructive":false,"covers":"*","description":"Escape hatch: call any RoninConv REST endpoint directly with this credential (same auth + permission checks as every other tool). Use only when no dedicated tool exists yet — e.g. brand-new endpoints. Consult get_docs for the route table.","inputSchema":{"type":"object","properties":{"method":{"type":"string","enum":["GET","POST","PATCH","PUT","DELETE"],"description":"HTTP method"},"path":{"type":"string","description":"Route path starting with \"/\", e.g. \"/links\". Never \"/mcp\"."},"query":{"type":"object","description":"Query parameters as a flat object (values stringified)"},"body":{"type":"object","description":"JSON request body for POST/PATCH/PUT"}},"required":["method","path"],"additionalProperties":false}},{"name":"bulk_action","title":"Bulk action","category":"platform","scope":"varies by resource","readOnly":false,"destructive":true,"covers":["POST /links/bulk","POST /landers/bulk","POST /offers/bulk","POST /traffic-sources/bulk","POST /pixels/bulk","POST /networks/bulk","POST /conversions/bulk","POST /domains/bulk","POST /meta/connections/bulk"],"description":"Multi-select operations on records: action \"patch\" (set fields on many rows), \"delete\" (permanent, with per-resource cleanup), or \"duplicate\" (clone the FIRST id; campaigns/landers/offers/traffic_sources/pixels/networks only). Patchable fields — campaigns: status|domain_id|traffic_source_id|ref|name; landers: status|name; offers: status|network|payout_hint|name|url_template; traffic_sources: status; pixels: status|name; networks: status|notes|name|template; conversions: status; domains: status; meta_connections: status|label. Note: this is the only way to archive (patch {\"status\":\"archived\"}), to delete records, and to edit offers. Ids outside your workspace are silently skipped (never touched, never cleaned up); the returned \"affected\" count is the number of rows actually changed. Pixel status changes and deletion immediately re-publish referencing campaign configs; paused/archived/deleted pixels cannot install new Meta browser events. Campaign name edits also refresh fallback Meta content_name. Deleting conversions is owner-only (needs conversions:delete, never granted to the editor role) - an editor-scoped actor gets 403 on resource \"conversions\", action \"delete\". Deleting meta_connections removes credentials only: spend, catalogs and campaign mappings survive. Shared accounts transfer to the oldest active surviving connection that saw them; otherwise they detach until rediscovered. All ids in the same delete are excluded from transfer.","inputSchema":{"type":"object","properties":{"resource":{"type":"string","enum":["campaigns","landers","offers","traffic_sources","pixels","networks","conversions","domains","meta_connections"],"description":"Record type"},"action":{"type":"string","enum":["patch","delete","duplicate"],"description":"Operation"},"ids":{"type":"array","items":{"type":"string","description":"record id"},"description":"Target record ids (max 1000; \"duplicate\" uses the first only)"},"patch":{"type":"object","description":"For action \"patch\": fields to set — see per-resource whitelist above"}},"required":["resource","action","ids"],"additionalProperties":false}},{"name":"list_campaigns","title":"List campaigns","category":"campaigns","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /links"],"description":"List campaigns (API object: \"links\") in the workspace, newest first (max 500). Each row: id (the public URL slug — ad URL is https://<domain>/<id>), name, ref, routing flow JSON, status (active|paused|archived), domain_id, traffic_source_id, meta_campaign_ids (canonical array of linked Meta campaign ids; empty when unlinked).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_n8n_map","title":"List n8n-mapped campaigns","category":"campaigns","scope":"edge:read","readOnly":true,"destructive":false,"covers":["GET /links/n8n-map"],"description":"Campaigns with an n8n webhook path set, updated since a given time. Ordered by updated_at (not created_at, unlike list_campaigns), so a change to an old campaign is never missed. Built for rp-edge to poll for lead-delivery routing config, not a workspace report. edge:read is explicit-opt-in: the \"full\" key scope does not include it.","inputSchema":{"type":"object","properties":{"since":{"type":"string","description":"ISO-8601 timestamp; only rows updated after this are returned (optional)"}},"additionalProperties":false}},{"name":"create_campaign","title":"Create campaign","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /links"],"description":"Create a campaign. Create landers/offers first, then wire them in the routing flow. The response id is the public slug: the ad URL is https://<campaign domain>/<id>. Flow definition — exactly one of three shapes (GRAPH is what the dashboard flow-builder reads/writes; prefer it):\n1. GRAPH: {\"graph\":{\"entry\":\"entry\",\"nodes\":{\"entry\":{\"id\":\"entry\",\"kind\":\"entry\"},\"n1\":{\"id\":\"n1\",\"kind\":\"lander\",\"ref\":\"<lander_id>\",\"priority\":1,\"when\":{...}},\"n2\":{\"id\":\"n2\",\"kind\":\"offer\",\"ref\":\"<offer_id>\"},\"n3\":{\"id\":\"n3\",\"kind\":\"path\",\"when\":{...}},\"n4\":{\"id\":\"n4\",\"kind\":\"filter\",\"when\":{...}}},\"edges\":[{\"id\":\"e1\",\"from\":\"entry\",\"to\":\"n1\",\"weight\":1},{\"id\":\"e2\",\"from\":\"n1\",\"to\":\"n2\",\"weight\":1},{\"id\":\"e3\",\"from\":\"n4\",\"to\":\"n2\",\"exit\":\"fail\"}]}}\n   PIXEL node: {\"id\":\"p1\",\"kind\":\"pixel\",\"ref\":\"px_…\",\"event\":\"Lead\",\"amount\":0}. An ordinary edge with exactly one pixel endpoint attaches it to the other node in EITHER direction; it is never a traffic route. Entry/path/filter attachments fire on actual arrival/traversal; lander attachments fire only after an accepted lead from that exact served lander (never page serve); offer attachments fire only on that selected offer node's attributed navigation conversion, not a sidecar conversion. Lander/offer matrices keep these same semantics for the selected row. Active Meta pixels directly attached to the served lander or its selected lander-matrix owner pair targeted browser events with CAPI; browser installation receives only public dataset/event/value/currency/content_name, never credentials. Accepted internal intake or a successful external intake followed by the signed handoff (or explicit __rc.acceptLead) is the lead boundary; page views and prepareLead never fire it. Hybrid eventID/event_id is exactly click_id; Meta deduplication by dataset + event + click_id intentionally collapses repeated same-event stages. Same-dataset/event lander attachments coalesce in first-attachment order, keeping that descriptor's amount. Other Meta attachments, routing-arrow pixels and unattached pixels stay server-only. A routing arrow may carry \"pixel_ids\":[\"p1\",…] referencing graph pixel NODE IDs, not px_… resource IDs; these fire on traversal of the selected arrow. Preserve its stable nonempty \"id\". No pixel-to-pixel edges, pixel fail exits, or pixel_ids on pixel endpoint attachments. Attachments never establish offer reachability or count as filter pass exits. A pixel with NO node or arrow attachments fires campaign-wide on conversion. Event and amount overrides live on the pixel node: blank amount uses zero for lead/traversal and actual payout for conversion; numeric zero is an explicit override. Other server-only node activities retain their independent node/activity retry deduplication.\n   A lander node may carry \"monetization_offer_id\":\"of_…\". This is a right-side association, not a routing edge: it never changes the outgoing CTA/navigation offer, weights, reachability or ordinary pixel activities. The associated offer must be active in the same workspace. Attach a sidecar conversion pixel with {\"from\":\"n1\",\"to\":\"p1\",\"attachment\":\"monetization\"} (either direction): exactly one concrete lander endpoint with monetization_offer_id and one pixel endpoint are required; matrices, navigation edges and other attachment values are rejected. It fires only on an attributed monetization-role conversion for that exact served lander and matching sidecar offer, never on page serve, accepted lead or a navigation conversion. An untagged edge between the same nodes may coexist and still fires on accepted lead. Sidecar pixels are S2S-only and never enter the browser lead descriptors. A lander matrix may carry the same offer default; a row omitting it inherits, a row setting it to null disables, and a row setting another of_… id overrides (tagged pixel attachments require concrete landers, not the matrix). Generic providers save but report automatic SDK tracking unsupported; Everflow activates only from verified matching metadata and hostname provenance, without an age deadline. Explicit eligibility and context checks still apply; previously published ineligible snapshots require an approved campaign republish after the underlying blocker is resolved.\n   Conversion filtering belongs on the pixel attachment edge, e.g. {\"from\":\"offerNode\",\"to\":\"p1\",\"conversion_events\":[\"upsell\",\"renewal\"]}. Up to 100 distinct exact case-sensitive names (1–100 characters each); omit or [] to keep all existing conversion behavior. Any selected name matches (OR); eventless conversions do not match a nonempty filter. Allowed on offer/offer-matrix attachments or concrete-lander monetization attachments only, never accepted-lead/arrival/traversal or routing-arrow attachments. Definitions from list_conversion_events feed the picker, but unknown/deleted names stay valid. Matching uses the accepted inbound conversions.event BEFORE the pixel node's independent outbound event/amount overrides. Filters freeze with the served journey; editing a campaign only changes newly served visits.\n   Traffic walks from the entry node. A condition (\"when\") and a priority rank always belong on the node being routed TO, never on the edge; true for path, lander, offer and filter alike. edge.when/edge.priority still work if an older saved graph has them there, but write new conditions on the node. Among a node's outgoing edges, group the eligible ones by the target's priority (absent = lowest/fallback tier) and walk tiers low to high; the first tier with a condition-matching target wins (a tier with only unconditional targets is itself a match); weight only ever splits traffic between targets sharing the same priority/tier, never across tiers. The walk ends at an offer node. A path with no lander before its offer = direct-link (302 to the offer). A \"when\" is {\"country\":[…],\"device\":[…],\"os\":[…]} (all present keys must match), {\"rule\":\"rl_…\"} for a named rule, or carries inline \"conds\" in the named-rule format ANDed with the dim keys — e.g. {\"conds\":[{\"type\":\"param\",\"key\":\"offer\",\"op\":\"equals\",\"value\":\"test\"}]} routes visitors whose ad URL has ?offer=test (the builder's \"custom UTM\" chip; type \"token\" slot 1 is its \"{t1}\" chip). Optional x/y per node are editor coordinates.\n   FILTER node: a deny by default, per campaign. A visitor matching its \"when\" gets a bare 404 (indistinguishable from a link that never existed) and NOTHING is recorded (no click, no visit, no cost), UNLESS its fail exit is wired (an edge out of it tagged \"exit\":\"fail\"), in which case that destination is served instead (eg. a fallback offer) and the visit proceeds normally. Conditions are the ordinary rule set, so geo / device / bot_ua / datacenter / moderator / suspicious / param / token / role all work. Rejected at save time if it has no \"when\" (it would 404/fail everyone), has nothing routing into it (it filters nothing), has more than one \"exit\":\"fail\" edge, or that edge targets itself or a pixel node. Dashboard preview walks past filters so the owner can still see the page. Two shapes, chosen by whether the filter node has a PASS edge (one with no \"exit\", or \"exit\" anything but \"fail\"): with no pass edge it is terminal; a match beats every other matching edge rather than taking a weighted share, so reaching it at all means a match, and no matching traffic leaks past it. WITH a pass edge it is chained; every visitor reaches it (the edge into it behaves like any other edge), a match still denies/fails exactly as above, and anyone who doesn't match continues down the pass edge to whatever follows (lander, path, offer, or another filter) same as a path node. Chaining is how \"campaign → filter → lander → offer\" is built as one straight line instead of two sibling branches off the same parent. The fail exit works identically in both shapes. Two node fields beyond \"when\": \"action\" (\"404\", the default, or \"challenge\") and, only when action is \"challenge\", a \"challenge\" object {type, timeout_ms, on_fail}. action=\"404\" is unchanged: immediate, nothing recorded, same fail-exit behavior as above applies. action=\"challenge\" instead serves the matching visitor a real interstitial page; a client-side check decides pass or fail from there (fail then follows the same fail-exit-or-deny rule), and ONLY a fail counts in report_filtered (a shown-but-unresolved challenge counts nowhere). challenge.type is one of \"click\" (must click Continue before timeout_ms elapses, else fail), \"honeypot\" (a decoy element invisible to a real visitor; touching it fails immediately, never touching it passes on timeout, benefit of the doubt to a slow human), \"timing\" (a click BEFORE timeout_ms fails, catching a script that never waited for the page to render, no upper bound), or \"motion\" (a click with no prior mouse/touch movement fails). challenge.timeout_ms: milliseconds, 500 to 120000. challenge.on_fail: \"404\" (the default: a second, ordinary hit on the same link, indistinguishable from any other 404) or \"cosmetic\" (looks like a 404 but stays a 200, no second request; use when the campaign genuinely cannot risk a real 404 reaching a legitimate but slow visitor). The pass destination is never present in the challenge page's markup or script; it is resolved only when a genuine trigger event fires, closing the gap where a JS-capable bot could otherwise read a pre-resolved URL out of the DOM without ever dispatching the event meant to prove it real.\n   MATRIX node: a lookup table — one node instead of N offer/lander nodes and N ruled edges. {\"kind\":\"matrix\",\"of\":\"offer\"|\"lander\",\"key\":{\"type\":\"token\",\"slot\":1}|{\"type\":\"param\",\"key\":\"offer\"},\"rows\":[{\"value\":\"abc\",\"ref\":\"<offer_or_lander_id>\"}],\"fallback\":\"<id>\"?}. The key is read from the ad URL (token slot resolves through the traffic source; param is the literal query key); rows match on equals and values must be unique; fallback is where a visitor matching no row goes (omit = those visitors dead-end). An offer matrix is terminal; a lander matrix needs one routing edge to what follows and every row continues down it. Matrix-bound pixel attachments keep accepted-lead semantics for landers and attributed-conversion semantics for offers. Expanded at sync into ordinary path/lander/offer nodes behind {\"conds\":[…]} edges; both row kinds keep their matrix bind, and copied outgoing arrows keep their original id, pixel_ids and exit metadata. Refused at save without rows, with an empty value/target or duplicate value, when nothing routes into it, or when an offer matrix has an outgoing routing edge / a lander matrix has none. Pixel attachments in either direction never count as matrix routes.\n2. TREE (legacy): {\"tree\":{...}} with nodes {type:\"split\"|\"lander\"|\"offer\"}.\n3. PATHS (legacy): {\"paths\":[{\"weight\":100,\"when\":null,\"landers\":[{\"id\":\"...\",\"weight\":100}],\"offers\":[{\"id\":\"...\",\"weight\":100}]}]}.\nValidation: the flow must reach at least one offer node or the API rejects it (400).\nDashboard tracking-link generation only: optional \"utm_defaults\" is a sparse object of string values (max 2000 chars each), keyed only by utm_content, utm_placement, utm_campaign, utm_source, utm_medium, utm_term. Nonblank values override enabled traffic-source placeholders by canonical role using each row's actual parameter name; unmapped roles append the canonical utm_* parameter. Explicitly disabled role rows suppress their defaults. Blank/whitespace-only values fall back to the source; the dashboard saves trimmed nonblank values. Defaults are literal, not recursively macro-expanded. Other source placeholders/platform macros stay unchanged. At generation, campaign-known RC macros in source placeholders expand: {campaign_id}, {link_id}, {campaign_name}, {bold_campaign_name}, {ref}, {traffic_source}, {ct1}…{ct10}; click-time and {{platform}} macros remain intact. This metadata neither enriches visits nor overrides incoming parameters and is not copied into KV routing.\nPlatform campaign-ID source fields (role \"campaign\" or param \"platform_campaign_id\") preserve {campaign_id} for the ad platform, including OpenAI. Use {link_id} for the RC slug in those fields.\nOptional routing extras (ride alongside graph/tree/paths): \"campaign_tokens\":[\"v1\",…] fill {ct1}…{ct10} in offer/postback URLs; \"lead_endpoint\":\"https://…\" is this campaign's form-endpoint override, injected into served landers as __rc.lead_endpoint (falls back to the workspace traffic-settings value; https only, or the 'rc-internal' sentinel to route this campaign's leads through RC's own capture first, then forward to rp-edge); \"vars\":{\"name\":\"value\",…} are campaign template vars — {{var:name}} placeholders in the lander HTML fill from them at serve time (also exposed as __rc.vars; unknown placeholders blank out; max 40 vars, names [a-zA-Z0-9_-], values ≤2000 chars) — one lander template, per-campaign copy. Chaining lander→lander→offer in a GRAPH makes a multi-page funnel with one click_id end to end; lander pixels wait for that stage's accepted lead, while node/arrow traversal pixels fire only for the segment actually served.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Display name shown in the dashboard"},"ref":{"type":"string","description":"Free-form reference carried on every event (e.g. an external campaign key); visitors can override per-visit via ?ref= on the ad URL. If a campaign source is configured, setting this to a campaign's Source ID also ties the two unless auto_link_by_ref is false; use false with an explicit attach_link_campaign choice"},"auto_link_by_ref":{"type":"boolean","description":"Default true: match ref to an Airtable Source ID. Set false when explicitly choosing a row via attach_link_campaign; ref is still saved unchanged"},"routing":{"type":"object","description":"Full routing flow; see the tool description for routing and pixel activity semantics","properties":{"utm_defaults":{"type":"object","properties":{"utm_content":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_placement":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_campaign":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_source":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_medium":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_term":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000}},"additionalProperties":false,"description":"Sparse canonical UTM-role defaults for dashboard tracking-link generation only. Canonical UTM parameter names also match source rows with no assigned role; see tool description"},"graph":{"type":"object","description":"Preferred flow graph; pixel attachments are non-routing in either direction","properties":{"entry":{"type":"string","description":"Ordinary graph node ID where traffic enters; never a pixel"},"nodes":{"type":"object","description":"Graph node ID → node. Pixel refs are workspace px_… resources.","additionalProperties":{"type":"object","description":"Flow node; event and amount overrides belong to pixel nodes","properties":{"id":{"type":"string","description":"Optional stable node ID; when supplied it must exactly match its nodes-map key"},"kind":{"type":"string","enum":["entry","path","filter","lander","offer","matrix","pixel"]},"ref":{"type":"string","description":"Workspace lander/offer/pixel resource ID for that node kind"},"monetization_offer_id":{"type":["string","null"],"description":"Active workspace of_… sidecar offer on a lander (or lander-matrix default); not CTA routing"},"event":{"type":"string","description":"Pixel node platform event override; omitted uses the pixel default"},"amount":{"type":["number","string","null"],"description":"Pixel value override, non-negative; blank/null/omitted uses zero for lead/traversal or actual conversion payout; zero is explicit"}}}},"edges":{"type":"array","items":{"type":"object","description":"Routing arrow, or an undirected non-routing attachment with one pixel endpoint","properties":{"id":{"type":"string","description":"Nonempty string unique across graph edges when supplied; required when pixel_ids is nonempty; retained through matrix expansion"},"from":{"type":"string","description":"Existing graph node ID"},"to":{"type":"string","description":"Existing graph node ID; pixel-to-pixel attachments are forbidden"},"weight":{"type":"number","description":"Routing weight within the selected priority tier","minimum":0},"exit":{"type":"string","enum":["fail"],"description":"Filter fail routing exit; never a pixel attachment"},"attachment":{"type":"string","enum":["monetization"],"description":"Sidecar conversion activity only: one pixel and one concrete lander with monetization_offer_id; omit for ordinary activity. Either direction; may coexist with an untagged attachment between the same nodes"},"pixel_ids":{"type":"array","items":{"type":"string","description":"Existing graph pixel node ID, NOT its px_… resource ref"},"description":"Pixels firing when this routing arrow is traversed; only on arrows without pixel endpoints","uniqueItems":true}}},"description":"Routing arrows plus bidirectional pixel endpoint attachments"}}}}},"id":{"type":"string","description":"Optional custom public slug; default: random unguessable 8 chars"},"status":{"type":"string","enum":["active","paused"],"description":"Default active"},"domain_id":{"type":"string","description":"Serving domain id (see list_domains); campaigns adopt the workspace default if unset"},"traffic_source_id":{"type":"string","description":"Traffic source id for S2S conversion feedback (see list_traffic_sources)"}},"required":["routing"],"additionalProperties":false}},{"name":"update_campaign","title":"Update campaign","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["PATCH /links/:id"],"description":"Patch a campaign: name, ref, routing flow, status (active|paused|archived), domain_id, traffic_source_id, workflow_status, and/or commercial-terms overrides. Omitted fields keep their value; domain_id/traffic_source_id/workflow_status/the two overrides clear on an empty string or null. Routing is re-validated and the edge route config re-syncs immediately. An unknown id (or one in another workspace) is a 404, never a silent no-op. Flow definition — exactly one of three shapes (GRAPH is what the dashboard flow-builder reads/writes; prefer it):\n1. GRAPH: {\"graph\":{\"entry\":\"entry\",\"nodes\":{\"entry\":{\"id\":\"entry\",\"kind\":\"entry\"},\"n1\":{\"id\":\"n1\",\"kind\":\"lander\",\"ref\":\"<lander_id>\",\"priority\":1,\"when\":{...}},\"n2\":{\"id\":\"n2\",\"kind\":\"offer\",\"ref\":\"<offer_id>\"},\"n3\":{\"id\":\"n3\",\"kind\":\"path\",\"when\":{...}},\"n4\":{\"id\":\"n4\",\"kind\":\"filter\",\"when\":{...}}},\"edges\":[{\"id\":\"e1\",\"from\":\"entry\",\"to\":\"n1\",\"weight\":1},{\"id\":\"e2\",\"from\":\"n1\",\"to\":\"n2\",\"weight\":1},{\"id\":\"e3\",\"from\":\"n4\",\"to\":\"n2\",\"exit\":\"fail\"}]}}\n   PIXEL node: {\"id\":\"p1\",\"kind\":\"pixel\",\"ref\":\"px_…\",\"event\":\"Lead\",\"amount\":0}. An ordinary edge with exactly one pixel endpoint attaches it to the other node in EITHER direction; it is never a traffic route. Entry/path/filter attachments fire on actual arrival/traversal; lander attachments fire only after an accepted lead from that exact served lander (never page serve); offer attachments fire only on that selected offer node's attributed navigation conversion, not a sidecar conversion. Lander/offer matrices keep these same semantics for the selected row. Active Meta pixels directly attached to the served lander or its selected lander-matrix owner pair targeted browser events with CAPI; browser installation receives only public dataset/event/value/currency/content_name, never credentials. Accepted internal intake or a successful external intake followed by the signed handoff (or explicit __rc.acceptLead) is the lead boundary; page views and prepareLead never fire it. Hybrid eventID/event_id is exactly click_id; Meta deduplication by dataset + event + click_id intentionally collapses repeated same-event stages. Same-dataset/event lander attachments coalesce in first-attachment order, keeping that descriptor's amount. Other Meta attachments, routing-arrow pixels and unattached pixels stay server-only. A routing arrow may carry \"pixel_ids\":[\"p1\",…] referencing graph pixel NODE IDs, not px_… resource IDs; these fire on traversal of the selected arrow. Preserve its stable nonempty \"id\". No pixel-to-pixel edges, pixel fail exits, or pixel_ids on pixel endpoint attachments. Attachments never establish offer reachability or count as filter pass exits. A pixel with NO node or arrow attachments fires campaign-wide on conversion. Event and amount overrides live on the pixel node: blank amount uses zero for lead/traversal and actual payout for conversion; numeric zero is an explicit override. Other server-only node activities retain their independent node/activity retry deduplication.\n   A lander node may carry \"monetization_offer_id\":\"of_…\". This is a right-side association, not a routing edge: it never changes the outgoing CTA/navigation offer, weights, reachability or ordinary pixel activities. The associated offer must be active in the same workspace. Attach a sidecar conversion pixel with {\"from\":\"n1\",\"to\":\"p1\",\"attachment\":\"monetization\"} (either direction): exactly one concrete lander endpoint with monetization_offer_id and one pixel endpoint are required; matrices, navigation edges and other attachment values are rejected. It fires only on an attributed monetization-role conversion for that exact served lander and matching sidecar offer, never on page serve, accepted lead or a navigation conversion. An untagged edge between the same nodes may coexist and still fires on accepted lead. Sidecar pixels are S2S-only and never enter the browser lead descriptors. A lander matrix may carry the same offer default; a row omitting it inherits, a row setting it to null disables, and a row setting another of_… id overrides (tagged pixel attachments require concrete landers, not the matrix). Generic providers save but report automatic SDK tracking unsupported; Everflow activates only from verified matching metadata and hostname provenance, without an age deadline. Explicit eligibility and context checks still apply; previously published ineligible snapshots require an approved campaign republish after the underlying blocker is resolved.\n   Conversion filtering belongs on the pixel attachment edge, e.g. {\"from\":\"offerNode\",\"to\":\"p1\",\"conversion_events\":[\"upsell\",\"renewal\"]}. Up to 100 distinct exact case-sensitive names (1–100 characters each); omit or [] to keep all existing conversion behavior. Any selected name matches (OR); eventless conversions do not match a nonempty filter. Allowed on offer/offer-matrix attachments or concrete-lander monetization attachments only, never accepted-lead/arrival/traversal or routing-arrow attachments. Definitions from list_conversion_events feed the picker, but unknown/deleted names stay valid. Matching uses the accepted inbound conversions.event BEFORE the pixel node's independent outbound event/amount overrides. Filters freeze with the served journey; editing a campaign only changes newly served visits.\n   Traffic walks from the entry node. A condition (\"when\") and a priority rank always belong on the node being routed TO, never on the edge; true for path, lander, offer and filter alike. edge.when/edge.priority still work if an older saved graph has them there, but write new conditions on the node. Among a node's outgoing edges, group the eligible ones by the target's priority (absent = lowest/fallback tier) and walk tiers low to high; the first tier with a condition-matching target wins (a tier with only unconditional targets is itself a match); weight only ever splits traffic between targets sharing the same priority/tier, never across tiers. The walk ends at an offer node. A path with no lander before its offer = direct-link (302 to the offer). A \"when\" is {\"country\":[…],\"device\":[…],\"os\":[…]} (all present keys must match), {\"rule\":\"rl_…\"} for a named rule, or carries inline \"conds\" in the named-rule format ANDed with the dim keys — e.g. {\"conds\":[{\"type\":\"param\",\"key\":\"offer\",\"op\":\"equals\",\"value\":\"test\"}]} routes visitors whose ad URL has ?offer=test (the builder's \"custom UTM\" chip; type \"token\" slot 1 is its \"{t1}\" chip). Optional x/y per node are editor coordinates.\n   FILTER node: a deny by default, per campaign. A visitor matching its \"when\" gets a bare 404 (indistinguishable from a link that never existed) and NOTHING is recorded (no click, no visit, no cost), UNLESS its fail exit is wired (an edge out of it tagged \"exit\":\"fail\"), in which case that destination is served instead (eg. a fallback offer) and the visit proceeds normally. Conditions are the ordinary rule set, so geo / device / bot_ua / datacenter / moderator / suspicious / param / token / role all work. Rejected at save time if it has no \"when\" (it would 404/fail everyone), has nothing routing into it (it filters nothing), has more than one \"exit\":\"fail\" edge, or that edge targets itself or a pixel node. Dashboard preview walks past filters so the owner can still see the page. Two shapes, chosen by whether the filter node has a PASS edge (one with no \"exit\", or \"exit\" anything but \"fail\"): with no pass edge it is terminal; a match beats every other matching edge rather than taking a weighted share, so reaching it at all means a match, and no matching traffic leaks past it. WITH a pass edge it is chained; every visitor reaches it (the edge into it behaves like any other edge), a match still denies/fails exactly as above, and anyone who doesn't match continues down the pass edge to whatever follows (lander, path, offer, or another filter) same as a path node. Chaining is how \"campaign → filter → lander → offer\" is built as one straight line instead of two sibling branches off the same parent. The fail exit works identically in both shapes. Two node fields beyond \"when\": \"action\" (\"404\", the default, or \"challenge\") and, only when action is \"challenge\", a \"challenge\" object {type, timeout_ms, on_fail}. action=\"404\" is unchanged: immediate, nothing recorded, same fail-exit behavior as above applies. action=\"challenge\" instead serves the matching visitor a real interstitial page; a client-side check decides pass or fail from there (fail then follows the same fail-exit-or-deny rule), and ONLY a fail counts in report_filtered (a shown-but-unresolved challenge counts nowhere). challenge.type is one of \"click\" (must click Continue before timeout_ms elapses, else fail), \"honeypot\" (a decoy element invisible to a real visitor; touching it fails immediately, never touching it passes on timeout, benefit of the doubt to a slow human), \"timing\" (a click BEFORE timeout_ms fails, catching a script that never waited for the page to render, no upper bound), or \"motion\" (a click with no prior mouse/touch movement fails). challenge.timeout_ms: milliseconds, 500 to 120000. challenge.on_fail: \"404\" (the default: a second, ordinary hit on the same link, indistinguishable from any other 404) or \"cosmetic\" (looks like a 404 but stays a 200, no second request; use when the campaign genuinely cannot risk a real 404 reaching a legitimate but slow visitor). The pass destination is never present in the challenge page's markup or script; it is resolved only when a genuine trigger event fires, closing the gap where a JS-capable bot could otherwise read a pre-resolved URL out of the DOM without ever dispatching the event meant to prove it real.\n   MATRIX node: a lookup table — one node instead of N offer/lander nodes and N ruled edges. {\"kind\":\"matrix\",\"of\":\"offer\"|\"lander\",\"key\":{\"type\":\"token\",\"slot\":1}|{\"type\":\"param\",\"key\":\"offer\"},\"rows\":[{\"value\":\"abc\",\"ref\":\"<offer_or_lander_id>\"}],\"fallback\":\"<id>\"?}. The key is read from the ad URL (token slot resolves through the traffic source; param is the literal query key); rows match on equals and values must be unique; fallback is where a visitor matching no row goes (omit = those visitors dead-end). An offer matrix is terminal; a lander matrix needs one routing edge to what follows and every row continues down it. Matrix-bound pixel attachments keep accepted-lead semantics for landers and attributed-conversion semantics for offers. Expanded at sync into ordinary path/lander/offer nodes behind {\"conds\":[…]} edges; both row kinds keep their matrix bind, and copied outgoing arrows keep their original id, pixel_ids and exit metadata. Refused at save without rows, with an empty value/target or duplicate value, when nothing routes into it, or when an offer matrix has an outgoing routing edge / a lander matrix has none. Pixel attachments in either direction never count as matrix routes.\n2. TREE (legacy): {\"tree\":{...}} with nodes {type:\"split\"|\"lander\"|\"offer\"}.\n3. PATHS (legacy): {\"paths\":[{\"weight\":100,\"when\":null,\"landers\":[{\"id\":\"...\",\"weight\":100}],\"offers\":[{\"id\":\"...\",\"weight\":100}]}]}.\nValidation: the flow must reach at least one offer node or the API rejects it (400).\nDashboard tracking-link generation only: optional \"utm_defaults\" is a sparse object of string values (max 2000 chars each), keyed only by utm_content, utm_placement, utm_campaign, utm_source, utm_medium, utm_term. Nonblank values override enabled traffic-source placeholders by canonical role using each row's actual parameter name; unmapped roles append the canonical utm_* parameter. Explicitly disabled role rows suppress their defaults. Blank/whitespace-only values fall back to the source; the dashboard saves trimmed nonblank values. Defaults are literal, not recursively macro-expanded. Other source placeholders/platform macros stay unchanged. At generation, campaign-known RC macros in source placeholders expand: {campaign_id}, {link_id}, {campaign_name}, {bold_campaign_name}, {ref}, {traffic_source}, {ct1}…{ct10}; click-time and {{platform}} macros remain intact. This metadata neither enriches visits nor overrides incoming parameters and is not copied into KV routing.\nPlatform campaign-ID source fields (role \"campaign\" or param \"platform_campaign_id\") preserve {campaign_id} for the ad platform, including OpenAI. Use {link_id} for the RC slug in those fields.\nOptional routing extras (ride alongside graph/tree/paths): \"campaign_tokens\":[\"v1\",…] fill {ct1}…{ct10} in offer/postback URLs; \"lead_endpoint\":\"https://…\" is this campaign's form-endpoint override, injected into served landers as __rc.lead_endpoint (falls back to the workspace traffic-settings value; https only, or the 'rc-internal' sentinel to route this campaign's leads through RC's own capture first, then forward to rp-edge); \"vars\":{\"name\":\"value\",…} are campaign template vars — {{var:name}} placeholders in the lander HTML fill from them at serve time (also exposed as __rc.vars; unknown placeholders blank out; max 40 vars, names [a-zA-Z0-9_-], values ≤2000 chars) — one lander template, per-campaign copy. Chaining lander→lander→offer in a GRAPH makes a multi-page funnel with one click_id end to end; lander pixels wait for that stage's accepted lead, while node/arrow traversal pixels fire only for the segment actually served.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Campaign id (public slug)"},"name":{"type":"string","description":"New display name"},"ref":{"type":"string","description":"New default ref; setting it to a campaign Source ID also ties this link to that campaign row when a campaign source is configured, unless auto_link_by_ref is false"},"auto_link_by_ref":{"type":"boolean","description":"Default true: match an updated ref to an Airtable Source ID. Set false when explicitly choosing a row via attach_link_campaign; ref is still saved unchanged"},"routing":{"type":"object","description":"Replacement routing JSON (full replace); same node/arrow pixel activity contract as create_campaign","properties":{"utm_defaults":{"type":"object","properties":{"utm_content":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_placement":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_campaign":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_source":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_medium":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000},"utm_term":{"type":"string","description":"Literal campaign tracking-link default; blank falls back to source","maxLength":2000}},"additionalProperties":false,"description":"Sparse canonical UTM-role defaults for dashboard tracking-link generation only. Canonical UTM parameter names also match source rows with no assigned role; see tool description"},"graph":{"type":"object","description":"Preferred flow graph; pixel attachments are non-routing in either direction","properties":{"entry":{"type":"string","description":"Ordinary graph node ID where traffic enters; never a pixel"},"nodes":{"type":"object","description":"Graph node ID → node. Pixel refs are workspace px_… resources.","additionalProperties":{"type":"object","description":"Flow node; event and amount overrides belong to pixel nodes","properties":{"id":{"type":"string","description":"Optional stable node ID; when supplied it must exactly match its nodes-map key"},"kind":{"type":"string","enum":["entry","path","filter","lander","offer","matrix","pixel"]},"ref":{"type":"string","description":"Workspace lander/offer/pixel resource ID for that node kind"},"monetization_offer_id":{"type":["string","null"],"description":"Active workspace of_… sidecar offer on a lander (or lander-matrix default); not CTA routing"},"event":{"type":"string","description":"Pixel node platform event override; omitted uses the pixel default"},"amount":{"type":["number","string","null"],"description":"Pixel value override, non-negative; blank/null/omitted uses zero for lead/traversal or actual conversion payout; zero is explicit"}}}},"edges":{"type":"array","items":{"type":"object","description":"Routing arrow, or an undirected non-routing attachment with one pixel endpoint","properties":{"id":{"type":"string","description":"Nonempty string unique across graph edges when supplied; required when pixel_ids is nonempty; retained through matrix expansion"},"from":{"type":"string","description":"Existing graph node ID"},"to":{"type":"string","description":"Existing graph node ID; pixel-to-pixel attachments are forbidden"},"weight":{"type":"number","description":"Routing weight within the selected priority tier","minimum":0},"exit":{"type":"string","enum":["fail"],"description":"Filter fail routing exit; never a pixel attachment"},"attachment":{"type":"string","enum":["monetization"],"description":"Sidecar conversion activity only: one pixel and one concrete lander with monetization_offer_id; omit for ordinary activity. Either direction; may coexist with an untagged attachment between the same nodes"},"pixel_ids":{"type":"array","items":{"type":"string","description":"Existing graph pixel node ID, NOT its px_… resource ref"},"description":"Pixels firing when this routing arrow is traversed; only on arrows without pixel endpoints","uniqueItems":true}}},"description":"Routing arrows plus bidirectional pixel endpoint attachments"}}}}},"status":{"type":"string","enum":["active","paused","archived"],"description":"New status"},"domain_id":{"type":["string","null"],"description":"Serving domain id; empty string or null clears it"},"traffic_source_id":{"type":["string","null"],"description":"Traffic source id; empty string or null clears it"},"workflow_status":{"type":["string","null"],"description":"Free-text pipeline status (e.g. Preparing/Testing/Scaling) separate from status, which stays purely behavioral; empty string or null clears it"},"revshare_pct_override":{"type":["number","null"],"description":"Overrides this campaign's advertiser network's default revshare_pct (0-100); null clears it back to the default"},"attribution_window_days_override":{"type":["integer","null"],"description":"Overrides this campaign's advertiser network's default attribution_window_days; null clears it back to the default"}},"required":["id"],"additionalProperties":false}},{"name":"link_meta_campaign","title":"Link Meta campaigns","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /links/:id/meta-link"],"description":"Replace the entire list of Meta (Facebook) campaigns backing a RoninConv campaign. Many Meta campaigns may back one RoninConv campaign; each Meta id belongs to at most one RoninConv campaign per workspace (409 on conflict, previous assignments preserved). Their ad spend is summed into report cost. Find ids with list_meta_campaigns. meta_campaign_ids is required; pass [] to unlink all. IDs are trimmed and deduplicated. New IDs must be discovered in this workspace; existing undiscovered assignments may be retained.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"RoninConv campaign id"},"meta_campaign_ids":{"type":"array","items":{"type":"string","description":"Meta campaign id"},"description":"Complete replacement list; [] unlinks all"}},"required":["id","meta_campaign_ids"],"additionalProperties":false}},{"name":"list_landers","title":"List landers","category":"landers","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /landers"],"description":"List landing pages (metadata only — fetch HTML with get_lander_html). Landers are versioned by (name, version); routing flows reference them by id.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_lander","title":"Create lander","category":"landers","scope":"landers:write","readOnly":false,"destructive":false,"covers":["POST /landers"],"description":"Upload a landing page as a self-contained HTML document, served at the edge with zero redirects. Template placeholders the router injects at serve time: {{OFFER_URL}} (the routed offer link with click_id — use as every CTA href), {{CLICK_ID}}. Add data-cta to CTA <a> tags so the click beacon fires automatically. To publish a new version of an existing lander, pass the same name with a higher version, then update campaign flows to the new lander id. (name, version) must be unique per workspace among non-archived landers; archiving a lander frees its name for reuse.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Lander name (stable across versions)"},"html":{"type":"string","description":"Complete HTML document, inline CSS/JS. EVERY asset must be a full https:// URL: a lander uploaded this way is served from KV verbatim, so the workspace Asset base URL does NOT rewrite it and a root-absolute or relative path (/images/hero.jpg, cover.png) resolves against the tracking domain and 404s. Root-absolute paths are for REPO-sourced landers only (set_lander_source) — which are also the only ones validate_lander and the drift badge can check."},"version":{"type":"integer","description":"Version number, default 1"}},"required":["name","html"],"additionalProperties":false}},{"name":"get_lander_html","title":"Get lander HTML","category":"landers","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /landers/:id/html"],"description":"Fetch one lander's stored HTML (plus metadata) so you can review or derive a new version from it.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Lander id (ld_…)"}},"required":["id"],"additionalProperties":false}},{"name":"get_templates","title":"Get workspace token templates","category":"platform","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /templates"],"description":"Per-workspace template defaults. networks = tracking-token query strings by network template (e.g. everflow), auto-applied to new network records and Everflow offer imports. sources = traffic-source token presets by platform kind (meta/tiktok/openai/beehiiv/coreg/hermes_sms/trackly_sms/custom), seeding new traffic sources. Empty objects mean the built-in defaults apply.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"set_templates","title":"Set workspace token templates","category":"platform","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /templates"],"description":"Replace the workspace token-template defaults (full object each time). Shape: {networks: {everflow: \"sub1={ct1}&sub2={ct2}&sub5={click_id}\"}, sources: {meta: {external_id_param, tokens: [{param, placeholder, alias, role, enabled}]}}}. Omitted keys fall back to built-ins; existing network/source records keep their saved tokens.\n`role` is what a token MEANS, and it is the contract templates resolve through: the optional first_name/last_name roles fill raw contact macros without fixed slots; the token holding role email_hash fills {email_hash} wherever that appears, whichever slot it sits in. Roles: utm_content, utm_placement, utm_campaign, utm_source, utm_medium, utm_term (UTM values; utm_term fills {utm_term} without a fixed slot, utm_source/utm_medium fill {utm_source}/{utm_medium}); ad, adset, campaign, placement, publisher, external_id, cost (platform attribution); email, phone, email_hash, phone_hash, lead_hash (the identity block). A role used twice is rejected. {t1}..{t20} still resolve positionally and are unchanged, but do not write new templates against them — a slot number moves, a role does not.","inputSchema":{"type":"object","properties":{"networks":{"type":"object","description":"Tracking-token query string per network template, e.g. {\"everflow\": \"sub1={ct1}&sub5={click_id}\"}"},"sources":{"type":"object","description":"Token preset per source kind: {meta: {external_id_param, tokens: [...]}, tiktok: …}"}},"additionalProperties":false}},{"name":"get_edge_settings","title":"Get Edge forwarding setting","category":"platform","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /edge-settings"],"description":"Whether this workspace queues conversions for Edge (rp-edge) UID-hash attribution, and which campaign refs or network ids are excluded from both conversion and internal RC lead forwarding even while the workspace forwards. Stored submitted visit identity takes precedence over callback identity. Default off; an unset workspace reads forward_to_edge: false, excluded_refs: [], excluded_networks: []. Capture never enables it.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"set_edge_settings","title":"Set Edge forwarding setting","category":"platform","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /edge-settings"],"description":"Turn Edge (rp-edge) forwarding on or off for this workspace, and/or set the campaign refs or network ids excluded from it. When on, every conversion that resolves a real workspace here (via a visit match or a per-network postback secret) queues a separate Edge delivery with stored visit identity first and callback identity only for missing fields. A ref in excluded_refs, or a network in excluded_networks (every campaign under it, present or future), is skipped even while the workspace forwards, for both this conversion path and internal RC lead forwarding (sendLeadToEdge); the same two lists cover both. Omit either list to leave it unchanged; pass [] to clear it. Capture races and delivery failures use bounded queue retries without changing the already-recorded conversion or replaying other feedback. The literal \"unmatched\" workspace never forwards. Enabling requires explicit operator approval; identity capture does not change this setting.","inputSchema":{"type":"object","properties":{"forward_to_edge":{"type":"boolean","description":"true to start forwarding, false to stop"},"excluded_refs":{"type":"array","items":{"type":"string","description":"Campaign ref"},"description":"Campaign refs excluded from both conversion and lead Edge forwarding even when forward_to_edge is on. Omit to leave the current list unchanged."},"excluded_networks":{"type":"array","items":{"type":"string","description":"Network id"},"description":"Network ids whose campaigns (present and future) are excluded from both conversion and lead Edge forwarding even when forward_to_edge is on. Omit to leave the current list unchanged."}},"required":["forward_to_edge"],"additionalProperties":false}},{"name":"get_lander_preview_url","title":"Get lander preview URL","category":"landers","scope":"entities:read","readOnly":false,"destructive":false,"covers":["GET /landers/:id/preview-url"],"description":"Publish (idempotently) and return a direct preview link for one lander, served by the edge router at /_lander/:id. The preview renders exactly like a real visit but mints no click and records nothing; CTA hrefs are stubbed to \"#\". host/url are the workspace's default tracking domain — null when no default is set, in which case use path on any router serving host.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Lander id (ld_…)"}},"required":["id"],"additionalProperties":false}},{"name":"list_offers","title":"List offers","category":"offers","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /offers"],"description":"List offers — the monetized destination URLs that routing flows terminate at.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_offer","title":"Create offer","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /offers"],"description":"Create an offer. url_template is the FULL final URL clicks are sent to — the offer's base URL plus the network's tracking tokens (network config.tracking, e.g. sub1={ref}&sub7={email_hash}&sub8={phone_hash}&sub9={lead_hash}&sub10={click_id}); include them yourself here. {click_id} is REQUIRED somewhere in the URL — it is the attribution join key the network must echo in postbacks. Placeholders filled at serve time: {click_id} {ref} {link_id}. Pass network_id (nw_…) to hard-link the offer to a network record; add external_id to record the network's own offer id (409 if that pair is already imported). Edit/archive later via bulk_action on resource \"offers\".","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Offer name"},"url_template":{"type":"string","description":"Full destination URL incl. tracking tokens, e.g. https://track.net/abc?sub1={ref}&sub7={email_hash}&sub8={phone_hash}&sub9={lead_hash}&sub10={click_id}"},"network":{"type":"string","description":"Affiliate network label (defaults to the network record's name when network_id is given)"},"network_id":{"type":"string","description":"Network record id (nw_… from list_networks) to hard-link the offer to"},"external_id":{"type":"string","description":"The network's own offer id (requires network_id; unique per network)"},"payout_hint":{"type":"number","description":"Expected payout per conversion (display only; real payout comes from postbacks)"}},"required":["name","url_template"],"additionalProperties":false}},{"name":"list_networks","title":"List networks","category":"offers","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /networks"],"description":"List affiliate networks (simple reporting objects grouping offers; template \"everflow\" or \"custom\" drives the postback URL builder).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_network","title":"Create network","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /networks"],"description":"Create an affiliate network with a unique cryptographic scoped postback secret. 201 includes postback_ready after KV readback. 503 may include the persisted id: call initialize_network_postback_secret with that id rather than recreating it. 409 if the name or slug already exists.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Network name"},"notes":{"type":"string","description":"Free-form notes"},"template":{"type":"string","enum":["everflow","custom","keap"],"description":"Postback template family (default custom)"},"slug":{"type":"string","description":"Stable slug this network books revenue under when forwarded to rp-edge (e.g. ttt/hs/jm/ifi for a keap-template advertiser); unique per workspace. Set it for every network, not just keap - an unset slug falls back to a generic bucket and loses per-network warehouse breakdown"}},"required":["name"],"additionalProperties":false}},{"name":"update_network","title":"Update network","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["PATCH /networks/:id"],"description":"Patch a network's name, notes, status, template, slug or config. config merges key-by-key; concurrent changes return 409: reload the network and retry a fresh request. For everflow-template networks set config.api_key to the Everflow AFFILIATE API key (Partner Portal → API) to unlock the everflow_* tools — a masked key echoed back never overwrites the real one. config.everflow_sdk_enabled is explicit opt-in (new networks default off). config.everflow_sdk_hostname must match authenticated Everflow redirect tracking domain discovery; unchanged legacy verified hosts remain supported. Verification records are server-owned. For keap-template networks (a Keap-integrated advertiser, holding all its fields here rather than a separate advertiser shape), set config.platform, keap_account_id, keap_sak (masked secret, same never-overwrite echo), click_id_field_id, opt_in_date_field_id, webinar_click_id_field_id, webinar_entry_date_field_id, revshare_pct (0-100), attribution_window_days (integer >= 0); the latter two are this advertiser's default, and a campaign can override either via update_campaign's revshare_pct_override/attribution_window_days_override.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Network id (nw_…)"},"name":{"type":"string","description":"New name"},"notes":{"type":"string","description":"New notes"},"status":{"type":"string","description":"New status, e.g. active|archived"},"template":{"type":"string","enum":["everflow","custom","keap"],"description":"Postback/integration template family"},"slug":{"type":"string","description":"Stable slug this network books revenue under when forwarded to rp-edge; unique per workspace. Set it for every network, not just keap"},"config":{"type":"object","description":"Config keys to merge, see description for the keap-template field set, or api_key/tracking/everflow_sdk_hostname for everflow (tracking default: sub1={ref}&sub7={email_hash}&sub8={phone_hash}&sub9={lead_hash}&sub10={click_id})"}},"required":["id"],"additionalProperties":false}},{"name":"everflow_discover_sdk_domains","title":"Discover Everflow SDK domains","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /networks/everflow/sdk-domains"],"description":"Read-only discovery using an unsaved affiliate API key or a workspace-owned network. Returns only public hostnames from authenticated redirect_tracking_url fields, never advertiser URLs or secrets. Does not save or enable the SDK. Multiple hosts require explicit selection. Forbidden/incomplete discovery fails closed.","inputSchema":{"type":"object","properties":{"network_id":{"type":"string","description":"Existing workspace network id"},"api_key":{"type":"string","description":"Optional unsaved affiliate key; server-side only"}},"additionalProperties":false}},{"name":"everflow_test_connection","title":"Test Everflow connection","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /networks/:id/everflow/test"],"description":"Verify an everflow-template network's affiliate API key against the live Everflow API (api.eflow.team). On success stores verified_at, the visible-offer count, and one consistent network_affiliate_id/network_id from /affiliates/users. It never requires or creates a network-admin credential. Concurrent key changes return retryable HTTP 409, never persist the old identity onto the new key. Run after setting config.api_key via update_network.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Network id (nw_…) with template everflow and config.api_key set"}},"required":["id"],"additionalProperties":false}},{"name":"everflow_list_offers","title":"List Everflow offers","category":"offers","scope":"offers:write","readOnly":true,"destructive":false,"covers":["GET /networks/:id/everflow/offers"],"description":"Runnable Everflow offers for this affiliate account, ranked by OUR revenue over the window (best earners first): external_id, name, approval/visibility, payout description, clicks/conversions/revenue/cvr, tracking_url (the raw Everflow link), and direct_link_enabled, tracking-domain id, redirect URL evidence, pagination truncation, and imported_offer_id when already imported into RoninConv. Redirect/advertiser URLs are never treated as verified SDK hostname provenance. The picker feed for everflow_import_offers.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Everflow network id (nw_…)"},"days":{"type":"integer","description":"Revenue ranking window in days, 1–365 (default 30)"}},"required":["id"],"additionalProperties":false}},{"name":"everflow_get_offer_url","title":"Get Everflow offer URL","category":"offers","scope":"offers:write","readOnly":true,"destructive":false,"covers":["GET /networks/:id/everflow/offer-url"],"description":"Fetch the explicit redirect_tracking_url for one authorized Everflow offer. Fails clearly when redirect metadata is missing; never falls back to an advertiser URL. Returns {offer_id, url}; append the network's tracking tokens (config.tracking) before saving as a RoninConv offer via create_offer.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Everflow network id (nw_…)"},"offer_id":{"type":"string","description":"Numeric Everflow offer id (external_id from everflow_list_offers)"}},"required":["id","offer_id"],"additionalProperties":false}},{"name":"everflow_import_offers","title":"Import Everflow offers","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /networks/:id/everflow/import"],"description":"Import up to 200 Everflow offers into RoninConv offers in one call — pulls each explicit redirect_tracking_url (never the direct advertiser tracking_url) and appends this network's tracking tokens. Missing redirect metadata fails that offer without overwriting it; attribution works. Precedence: the network's own config.tracking, else the workspace's Everflow token template (Settings → Token templates), else the built-in default (sub1={ref}&sub7={email_hash}&sub8={phone_hash}&sub9={lead_hash}&sub10={click_id}). Idempotent per (network, external_id): re-importing updates name/URL/payout in place. Returns imported/updated/failed counts.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Everflow network id (nw_…)"},"offer_ids":{"type":"array","items":{"type":"string","description":"Everflow network_offer_id (external_id from everflow_list_offers)"},"description":"Everflow offer ids to import (max 200)"}},"required":["id","offer_ids"],"additionalProperties":false}},{"name":"everflow_list_pixels","title":"List Everflow postbacks","category":"offers","scope":"offers:write","readOnly":true,"destructive":false,"covers":["GET /networks/:id/everflow/pixels"],"description":"Existing affiliate postbacks/pixels registered inside Everflow, with ours=true on entries pointing at a RoninConv /pb endpoint — shows whether conversion feedback is already wired before calling everflow_register_postback.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Everflow network id (nw_…)"}},"required":["id"],"additionalProperties":false}},{"name":"everflow_register_postback","title":"Register Everflow postback","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /networks/:id/everflow/postback"],"description":"Register RoninConv's conversion postback INSIDE Everflow (global conversion pixel, or one offer via network_offer_id) so conversions flow back automatically: https://<host>/pb?secret=…&txid={transaction_id}&payout={payout_amount}&offer_id={offer_id}&provider_event_id={event_id}&email_hash={sub7}&phone_hash={sub8}&lead_hash={sub9}&click_id={sub10}. {sub10} is only the DEFAULT click_id macro — it actually mirrors whichever query param this network's tracking tokens assign to click_id (same precedence as everflow_import_offers: the network's own config.tracking, else the workspace's Everflow token template, else the built-in default). Click/contact/hash return macros follow that mapping. provider_event_id is numeric identity only; it does not set the business event name or change feedback defaults. Pending retries reconcile the ORIGINAL scope/URL/credential fingerprint before any new write; legacy or changed-key ambiguity requires administrator reconciliation. Host defaults to the workspace's first active domain. Uses the stored affiliate API key, reuses an active matching URL and conversion/offer scope, and returns verified:true only after provider readback. Concurrent installs are serialized; ambiguous writes are never blindly repeated. Missing key/domain/per-network secret/token mapping or denied provider permissions fail without claiming success. No Worker-wide secret fallback: initialize_network_postback_secret initializes or repairs a network. Existing callbacks are never edited; a changed active same-scope /pb?secret callback blocks installation. Manually replace its URL in Everflow instead of creating a duplicate (including single-offer callbacks). Everflow requires the returned offer to match a unique external ID in this authenticated network/workspace and an immutable visit association; invalid or unresolved attribution retries to the existing DLQ without fallback. Transaction/offer/event-type dedupe is preserved; repeated purchases of the same event type in one transaction are not separate occurrences. Non-Everflow callback behavior is unchanged.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Everflow network id (nw_…)"},"host":{"type":"string","description":"Router hostname to receive postbacks (must be an active workspace domain; default: first active)"},"network_offer_id":{"type":"string","description":"Limit the postback to one Everflow offer (default: global)"}},"required":["id"],"additionalProperties":false}},{"name":"initialize_network_postback_secret","title":"Initialize or repair network postback secret","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /networks/:id/postback-secret/initialize"],"description":"Initialize a missing cryptographic per-network secret or republish its existing workspace/network KV mapping without rotating it. Returns ready only after scoped KV readback; 503 means publication is unavailable/not yet visible: retry this tool, not create_network or rotation. Does not change provider callbacks, revoke legacy ingress, or return the secret. Use get_postback_info afterwards.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Network id (nw_…)"}},"required":["id"],"additionalProperties":false}},{"name":"rotate_network_postback_secret","title":"Rotate network postback secret","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /networks/:id/postback-secret/rotate"],"description":"Generate a new per-network secret for /pb postbacks and swap it in, the old secret stops validating immediately (its KV entry is deleted before the new one is written), so any postback URL still registered with it starts failing right away. A missing-secret network can instead use initialize_network_postback_secret without destructive rotation. Everflow installation requires this network-specific secret; Worker-wide callbacks cannot identify a network. After rotating, manually replace the existing Everflow callback URL (the installer blocks duplicate same-scope legacy callbacks), or update the URL on any other network’s own side.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Network id (nw_…)"}},"required":["id"],"additionalProperties":false}},{"name":"get_cloudflare_connection","title":"Get Cloudflare connection","category":"domains","scope":"domains:write","readOnly":true,"destructive":false,"covers":["GET /cloudflare"],"description":"Cloudflare integration status for this workspace — the account domains are attached under. Returns connected, auth (oauth|token), account_id/account_name, token_last4, expires_at, oauth_available (server has an OAuth client) and platform_fallback (true = no workspace connection yet, so attaches still run on the platform-wide legacy account). The credential itself is never returned.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"set_cloudflare_credentials","title":"Connect Cloudflare (API token)","category":"domains","scope":"domains:write","readOnly":false,"destructive":false,"covers":["PUT /cloudflare"],"description":"Connect this workspace to a Cloudflare account with an API token (needs Account:Read, Zone:Read, DNS:Edit and Zone WAF:Edit). Omit token to keep the stored credential and only switch account_id. The token is validated against Cloudflare before it is saved and is write-only afterwards (only its last 4 chars are ever returned).","inputSchema":{"type":"object","properties":{"token":{"type":"string","description":"Cloudflare API token. Omit to keep the currently stored credential."},"account_id":{"type":"string","description":"Cloudflare account id to attach domains under (see list_cloudflare_accounts). Optional when the token can see exactly one account."}},"additionalProperties":false}},{"name":"list_cloudflare_accounts","title":"List Cloudflare accounts","category":"domains","scope":"domains:write","readOnly":true,"destructive":false,"covers":["GET /cloudflare/accounts"],"description":"List the Cloudflare accounts the connected credentials can see — pick one for set_cloudflare_credentials.account_id.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"test_cloudflare_connection","title":"Test Cloudflare connection","category":"domains","scope":"domains:write","readOnly":true,"destructive":false,"covers":["POST /cloudflare/test"],"description":"Probe the workspace's Cloudflare credentials without changing anything: are they live, is the selected account still visible, and can Workers domains be read on it (the permission attach_domain needs). Returns {ok, source, account, workers, error}.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"disconnect_cloudflare","title":"Disconnect Cloudflare","category":"domains","scope":"domains:write","readOnly":false,"destructive":true,"covers":["POST /cloudflare/disconnect"],"description":"Drop the workspace's stored Cloudflare credentials. Already-attached domains keep serving, but attaching, verifying and detaching stop working until it is reconnected.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_domains","title":"List domains","category":"domains","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /domains"],"description":"List serving domains attached to the router. Each: id, hostname, status (active|warming|burned|retired), is_default. Campaign URLs are https://<hostname>/<campaign id>.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_domain_zones","title":"List Cloudflare zones","category":"domains","scope":"domains:write","readOnly":true,"destructive":false,"covers":["GET /domains/zones"],"description":"List DNS zones in the Cloudflare account this workspace has connected. These are the CLIENT-side zones — where tracking CNAMEs get published and where WAF rules apply. 400s when Cloudflare is not connected (see get_cloudflare_connection).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_workspace_subdomain","title":"Create workspace subdomain","category":"domains","scope":"domains:write","readOnly":false,"destructive":false,"covers":["POST /domains/subdomain"],"description":"Provision this workspace's own subdomain on the RoninConv platform domain (<workspace>.<platform domain>) and attach it straight to the edge router. DNS and SSL are automatic and live in ~1 minute — the workspace needs no Cloudflare account, publishes no DNS and waits on no certificate. This is the zero-setup default serving host; attach_domain is the white-label upgrade for a client-owned hostname. One per workspace; re-running repairs the attachment rather than creating a second. A label the platform reserves (api, mcp, www, app, assets, cdn, customers and similar) is refused with a 400: those hostnames serve the control plane, the dashboard or every lander’s images, and attaching one here would repoint it at the router.","inputSchema":{"type":"object","properties":{"slug":{"type":"string","description":"Override the subdomain label (defaults to the workspace id). Platform-reserved labels are refused."}},"additionalProperties":false}},{"name":"attach_domain","title":"Attach domain","category":"domains","scope":"domains:write","readOnly":false,"destructive":false,"covers":["POST /domains"],"description":"Provision a tracking hostname as a Cloudflare for SaaS custom hostname on the platform zone that fronts the edge router. Use a SUBDOMAIN (t.example.com) — bare apex domains need Cloudflare apex proxying and are rejected. Returns dns_records: the CNAME + validation TXTs that must exist for it to go live. Set create_dns to publish them automatically into the workspace's connected Cloudflare account (see get_cloudflare_connection.can_write_dns); otherwise hand dns_records to whoever runs DNS. The domain reports status \"warming\" until verify_domain sees hostname + certificate active.","inputSchema":{"type":"object","properties":{"hostname":{"type":"string","description":"Full hostname to serve campaigns on, e.g. \"t.example.com\" (subdomain required)"},"create_dns":{"type":"boolean","description":"Publish the DNS automatically in the workspace's connected Cloudflare account"}},"required":["hostname"],"additionalProperties":false}},{"name":"publish_domain_dns","title":"Publish domain DNS","category":"domains","scope":"domains:write","readOnly":false,"destructive":false,"covers":["POST /domains/:id/dns"],"description":"Publish (or repair) an attached domain's CNAME + validation TXT records in the workspace's connected Cloudflare account. Use when the domain was attached before Cloudflare was connected, or the records drifted. Also returns dns_records for manual setup.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Domain id (dm_…)"}},"required":["id"],"additionalProperties":false}},{"name":"verify_domain","title":"Verify domain","category":"domains","scope":"domains:write","readOnly":false,"destructive":false,"covers":["POST /domains/:id/verify"],"description":"Re-check a domain against Cloudflare. Returns host_status (hostname activation) and ssl_status (certificate issuance) — both must read \"active\" before the domain serves. Sets the row to active when live, warming while validation is pending, burned on error, and returns the dns_records still needed.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Domain id (dm_…)"}},"required":["id"],"additionalProperties":false}},{"name":"delete_domain","title":"Delete domain","category":"domains","scope":"domains:write","readOnly":false,"destructive":true,"covers":["DELETE /domains/:id"],"description":"Remove the custom hostname from Cloudflare and delete the domain, cleaning up the CNAME in the client zone when RoninConv created it. Campaigns pointing at it fall back to no domain (their URLs on that hostname stop working). Irreversible.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Domain id (dm_…)"}},"required":["id"],"additionalProperties":false}},{"name":"get_waf_rules","title":"Get Cloudflare WAF rules","category":"domains","scope":"domains:write","readOnly":true,"destructive":false,"covers":["GET /cloudflare/waf"],"description":"Read the Cloudflare WAF custom rules on one of the workspace's own zones (zone ids from list_domain_zones). These run at Cloudflare's edge, before traffic reaches the router — distinct from get_traffic_settings, which the router enforces itself. An empty list means the zone has no custom rules yet.","inputSchema":{"type":"object","properties":{"zone_id":{"type":"string","description":"Cloudflare zone id (see list_domain_zones)"}},"required":["zone_id"],"additionalProperties":false}},{"name":"set_waf_rules","title":"Set Cloudflare WAF rules","category":"domains","scope":"domains:write","readOnly":false,"destructive":false,"covers":["PUT /cloudflare/waf"],"description":"REPLACE the Cloudflare WAF custom rules on one of the workspace's own zones — the list you send becomes the whole rule set, so read get_waf_rules first and send the full list back with your edits. Expressions use Cloudflare's firewall syntax, e.g. (cf.client.bot), (ip.geoip.country eq \"RU\"), (cf.threat_score gt 30).","inputSchema":{"type":"object","properties":{"zone_id":{"type":"string","description":"Cloudflare zone id (see list_domain_zones)"},"rules":{"type":"array","items":{"type":"object","description":"One WAF custom rule","properties":{"action":{"type":"string","enum":["block","managed_challenge","js_challenge","challenge","log","skip"],"description":"What Cloudflare does when the expression matches"},"expression":{"type":"string","description":"Cloudflare firewall expression, e.g. (cf.client.bot)"},"description":{"type":"string","description":"Human label for the rule"},"enabled":{"type":"boolean","description":"Defaults to true"}}},"description":"The complete rule list for the zone (replaces what is there)"}},"required":["zone_id","rules"],"additionalProperties":false}},{"name":"set_default_domain","title":"Set default domain","category":"domains","scope":"domains:write","readOnly":false,"destructive":false,"covers":["POST /domains/set-default"],"description":"Set (or clear with id null) the workspace default serving domain — new campaigns adopt it automatically.","inputSchema":{"type":"object","properties":{"id":{"type":["string","null"],"description":"Domain id, or null to clear"}},"additionalProperties":false}},{"name":"list_traffic_sources","title":"List traffic sources","category":"traffic-sources","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /traffic-sources"],"description":"List traffic sources — S2S conversion feedback to ad platforms (TikTok Events API, Meta CAPI, OpenAI/ChatGPT Ads CAPI, beehiiv [tokens only], Coreg, custom). Secrets are masked. Includes delivered/failed counts.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_traffic_source","title":"Create traffic source","category":"traffic-sources","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /traffic-sources"],"description":"Create a traffic source. config keys by kind — tiktok: {pixel_id, access_token, event_name (default CompletePayment), test_event_code?}; openai: {pixel_id, access_token, event_name (default order_created), site_url?, currency? (default USD)}; coreg: native Coreg exit attribution, 20 default slots and coreg_txid={{txid}}; supplied config keys override defaults, url_template defaults null. coreg/hermes_sms/trackly_sms/custom: {url_template}: the postback URL fired on conversion, which must start with https:// and may contain {external_id} (original source transaction), {click_id} {txid} {payout} {ref} {link_id}; rejected at save time if it does not. New custom, Hermes SMS (hermes_sms) and Trackly SMS (trackly_sms) sources with omitted config.tokens receive 20 slots: t1–t5 are enabled UTM rows (utm_content, utm_placement, utm_campaign, utm_source, utm_medium), t6–t14 are blank, t15 is a disabled separator, and t16–t20 are enabled email, phone, email_hash, phone_hash, lead_hash rows with matching roles and aliases. All placeholders are blank. Assign the optional utm_term role to any token to fill {utm_term} in click and conversion templates. Explicit token arrays including [] are preserved. Null/omitted url_template fires nothing. Hermes SMS and Trackly SMS are native attribution/token platforms; no automatic cost sync is available yet. meta is attribution/tokens only — Meta conversions fire via a Pixel (see create_pixel / create_meta_pixel) added as a pixel node to a campaign flow, not inline here. beehiiv is tokens only for now (no S2S or cost integration yet; nothing fires on conversion). Attach the source to campaigns (traffic_source_id) so matched conversions fire S2S using the platform click id (ttclid / oppref).","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Source name"},"kind":{"type":"string","enum":["tiktok","meta","openai","beehiiv","coreg","hermes_sms","trackly_sms","custom"],"description":"Platform"},"config":{"type":"object","description":"Platform config (see tool description). access_token is stored server-side and never returned."}},"required":["name","kind"],"additionalProperties":false}},{"name":"update_traffic_source","title":"Update traffic source","category":"traffic-sources","scope":"links:write","readOnly":false,"destructive":false,"covers":["PATCH /traffic-sources/:id"],"description":"Patch a traffic source's name, status (active|paused|archived) or config. kind: coreg, hermes_sms or trackly_sms (send kind only, no other changes) explicitly adopts an existing custom source in place, preserving ID, name, status, full config and campaign references. Repeating the same native kind is safe; transitions from another native kind are rejected. No defaults or deliveries are applied, and names never classify sources. config merges key-by-key; a masked token echoed back never overwrites the real one. A supplied url_template is validated the same way as on create; null clears it (a custom source then keeps its tokens and fires nothing).","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Traffic source id (ts_…)"},"name":{"type":"string","description":"New name"},"kind":{"type":"string","enum":["coreg","hermes_sms","trackly_sms"],"description":"Adopt a custom source as Coreg, Hermes SMS or Trackly SMS without replacing its ID or config"},"status":{"type":"string","enum":["active","paused","archived"],"description":"New status"},"config":{"type":"object","description":"Config keys to merge (see create_traffic_source)"}},"required":["id"],"additionalProperties":false}},{"name":"test_traffic_source","title":"Test traffic source","category":"traffic-sources","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /traffic-sources/:id/test"],"description":"Fire a synthetic test conversion through the real S2S delivery path and return the platform's response (also logged to deliveries). Optionally supply platform click ids.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Traffic source id (ts_…)"},"ttclid":{"type":"string","description":"TikTok click id for the test event"},"fbclid":{"type":"string","description":"Meta click id for the test event"},"oppref":{"type":"string","description":"OpenAI click id for the test event"}},"required":["id"],"additionalProperties":false}},{"name":"list_source_deliveries","title":"List source deliveries","category":"traffic-sources","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /traffic-sources/:id/deliveries"],"description":"Last 50 S2S delivery attempts for one traffic source: timestamp, conversion id, HTTP status, ok flag, response detail — the debugging view for conversion feedback.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Traffic source id (ts_…)"}},"required":["id"],"additionalProperties":false}},{"name":"list_pixels","title":"List pixels","category":"pixels","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /pixels"],"description":"List conversion pixels — first-class CAPI targets (meta, google, chatgpt, tiktok). Each has platform, pixel_id, and delivered/failed CAPI counts. Flow attachments select activity; active Meta lander attachments also pair browser events with CAPI. Credentials are masked.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_pixel","title":"Create pixel","category":"pixels","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /pixels"],"description":"Register a conversion pixel from an existing platform pixel id. config keys by platform — meta: {capi_token, default_event (default Purchase), test_event_code?, currency?, content_name?}; tiktok: {access_token, default_event (default CompletePayment), test_event_code?, currency?} with pixel_id = event_source_id; chatgpt: {access_token, default_event (default order_created), site_url?, currency?} with pixel_id = pid; google: {customer_id, conversion_action_id, currency?} + connection_id (a gc_… Google connection supplies the OAuth refresh token; fires via the Data Manager API on gclid). The pixel fires when added as a pixel node to a campaign flow (link routing.graph). Active Meta pixels directly attached to a served lander or lander matrix pair targeted browser events with CAPI only after accepted lead intake; no PageView is sent and tokens remain server-side. To CREATE a brand-new pixel on Meta itself, use create_meta_pixel instead.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Pixel name"},"platform":{"type":"string","enum":["meta","tiktok","google","chatgpt","n8n"],"description":"Ad platform, or n8n to send the conversion to a workflow"},"pixel_id":{"type":"string","description":"Platform pixel / dataset id (event_source_id for tiktok, pid for chatgpt, customer_id for google)"},"ad_account_id":{"type":"string","description":"Owning ad account id (act_…)"},"connection_id":{"type":"string","description":"Connection id this pixel belongs to (mc_… for Meta, gc_… for Google)"},"config":{"type":"object","description":"Platform config — secrets (capi_token/access_token/refresh_token/developer_token) stored server-side, never returned"}},"required":["name"],"additionalProperties":false}},{"name":"update_pixel","title":"Update pixel","category":"pixels","scope":"links:write","readOnly":false,"destructive":false,"covers":["PATCH /pixels/:id"],"description":"Patch a pixel's name, status (active|paused|archived), pixel_id, or config. config merges key-by-key; a masked CAPI token echoed back never overwrites the real one. Referencing campaign edge configs re-publish immediately, including public Meta dataset, default event, currency/content_name changes and removal when paused/archived. Active direct lander/lander-matrix attachments pair targeted browser and CAPI events at accepted lead intake using click_id exactly as both event IDs; other attachments stay server-only. Secrets are never published.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Pixel id (px_…)"},"name":{"type":"string","description":"New name"},"status":{"type":"string","enum":["active","paused","archived"],"description":"New status"},"pixel_id":{"type":"string","description":"Platform pixel id"},"config":{"type":"object","description":"Config keys to merge"}},"required":["id"],"additionalProperties":false}},{"name":"test_pixel","title":"Test pixel","category":"pixels","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /pixels/:id/test"],"description":"Fire a synthetic test conversion through the real CAPI path and return the platform response and sanitized retained outbound request (also logged to pixel deliveries).","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Pixel id (px_…)"},"event_name":{"type":"string","description":"Event to fire (default the pixel's default_event)"},"click_id":{"type":"string","description":"Click id for the test event — injected as the platform's param (fbclid/gclid/oppref/ttclid)"},"fbclid":{"type":"string","description":"Deprecated alias for click_id (Meta)"}},"required":["id"],"additionalProperties":false}},{"name":"list_pixel_deliveries","title":"List pixel deliveries","category":"pixels","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /pixels/:id/deliveries"],"description":"Last 50 CAPI delivery attempts for one pixel: timestamp, conversion id, event name, HTTP status, ok flag, response detail, node/trigger IDs and sanitized outbound request snapshot. Request body is bounded at 64 KiB with explicit truncation/byte metadata; authentication is redacted. Arbitrary bodies/context require additional reports:identity permission (owner by default); report-only actors receive a safe projection. Historical/missing snapshots are null.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Pixel id (px_…)"}},"required":["id"],"additionalProperties":false}},{"name":"list_meta_pixels","title":"List Meta pixels","category":"pixels","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /meta/pixels"],"description":"Meta pixels discovered through the cost connections (for import) plus the ad accounts available to create a new pixel under. `imported` flags pixels already registered as RoninConv pixels.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_meta_pixel","title":"Create pixel on Meta","category":"pixels","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /meta/pixels/create"],"description":"Create a brand-new pixel ON Meta under an ad account (POST /act_/adspixels using that account's connection token), then register it as a RoninConv pixel in one step. Requires the connection token to have ads_management + business_management; returns a clear hint if not.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"New pixel name"},"ad_account_id":{"type":"string","description":"Ad account id (act_…) to create it under"}},"required":["name","ad_account_id"],"additionalProperties":false}},{"name":"list_google_connections","title":"List Google connections","category":"pixels","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /google/connections"],"description":"The workspace's Google Ads connections (multiple allowed) with masked developer tokens, plus whether OAuth and an app-level developer token are configured on the server. Add a connection via the browser OAuth flow (Traffic Sources → Google).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"update_google_connection","title":"Update Google connection","category":"pixels","scope":"links:write","readOnly":false,"destructive":false,"covers":["PATCH /google/connections/:id"],"description":"Set a Google connection's label, developer_token (masked once stored; falls back to the app-level token when unset), login_customer_id (MCC), and default customer_id/account_name. The OAuth refresh token comes from the connect flow.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Google connection id (gc_…)"},"label":{"type":"string","description":"Connection label"},"developer_token":{"type":"string","description":"Google Ads API developer token"},"login_customer_id":{"type":"string","description":"Manager (MCC) account id, digits only — optional"},"customer_id":{"type":"string","description":"Default Google Ads customer id, digits only"},"account_name":{"type":"string","description":"Display name for the account"}},"required":["id"],"additionalProperties":false}},{"name":"disconnect_google_connection","title":"Disconnect Google connection","category":"pixels","scope":"links:write","readOnly":false,"destructive":true,"covers":["POST /google/connections/:id/disconnect"],"description":"Remove one Google Ads connection (its refresh token, developer token, account).","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Google connection id (gc_…)"}},"required":["id"],"additionalProperties":false}},{"name":"list_google_accounts","title":"List Google Ads accounts","category":"pixels","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /google/connections/:id/accounts"],"description":"Google Ads accounts (customer ids) a connection can reach (listAccessibleCustomers). Needs the connection authorized + a developer token.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Google connection id (gc_…)"}},"required":["id"],"additionalProperties":false}},{"name":"list_google_conversion_actions","title":"List Google conversion actions","category":"pixels","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /google/connections/:id/conversion-actions"],"description":"UPLOAD_CLICKS conversion actions in a Google Ads account under a connection — the \"pixels\" you fire gclid conversions against. Register one as a RoninConv pixel with create_pixel (platform=google, connection_id, config.conversion_action_id + customer_id).","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Google connection id (gc_…)"},"customer_id":{"type":"string","description":"Google Ads customer id (digits only); defaults to the connection customer_id"}},"required":["id"],"additionalProperties":false}},{"name":"create_google_conversion_action","title":"Create Google conversion action","category":"pixels","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /google/connections/:id/conversion-actions/create"],"description":"Create an UPLOAD_CLICKS conversion action (\"pixel\") in a Google Ads account under a connection via the Ads API, then register it as a RoninConv pixel with create_pixel. Requires the connection authorized + a developer token.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Google connection id (gc_…)"},"name":{"type":"string","description":"Conversion action name"},"customer_id":{"type":"string","description":"Google Ads customer id (digits only); defaults to the connection customer_id"},"currency":{"type":"string","description":"Default currency (default USD)"},"category":{"type":"string","description":"Conversion category (default DEFAULT)"}},"required":["id","name"],"additionalProperties":false}},{"name":"get_report_settings","title":"Get report settings","category":"reporting","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /report/settings"],"description":"Get only the current workspace’s report presets and the current user’s global visible dimension chips. Returns {presets:[{id,name,dims,is_default?}],visible_chips:[dimension keys]}. A preset marked is_default opens on fresh report entry; otherwise Campaign → Offer, then the first preset. Unset values use shared defaults; API keys receive default visible chips. No other workspace settings are exposed.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"set_report_presets","title":"Save workspace report presets","category":"reporting","scope":"members:manage","readOnly":false,"destructive":false,"covers":["PUT /report/presets"],"description":"Replace only the workspace report presets, preserving unrelated settings atomically. Requires the workspace administration permission members:manage (owner role). Up to 50 unique ids; each preset contains 1–4 unique known dimensions. Optional is_default:true selects the workspace default (at most one). Default applies only to fresh report entry, not explicit report URLs. Without a marked default, Campaign → Offer is used if present, otherwise the first preset. Empty presets open a blank report. Returns {presets}.","inputSchema":{"type":"object","properties":{"presets":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique stable preset id","pattern":"^[a-zA-Z0-9_-]{1,80}$"},"name":{"type":"string","description":"Display name","minLength":1,"maxLength":100},"dims":{"type":"array","items":{"type":"string","enum":["link","ref","meta_campaign","meta_adset","meta_ad","day","country","region","city","continent","timezone","postal","colo","device","os","browser","os_version","browser_version","device_vendor","device_model","isp","asn","language","http_protocol","tls_version","suspicious","lander","offer","stage","event","network","lander_node","utm_source","utm_medium","utm_campaign","utm_content","utm_term","utm_placement","placement","publisher"]},"minItems":1,"maxItems":4,"uniqueItems":true},"is_default":{"type":"boolean","description":"Open this preset on fresh report entry; at most one preset may be default"}},"required":["id","name","dims"],"additionalProperties":false},"maxItems":50}},"required":["presets"],"additionalProperties":false}},{"name":"update_report_preferences","title":"Save my report chips","category":"reporting","scope":"preferences:write","readOnly":false,"destructive":false,"covers":["PATCH /me/report-preferences"],"description":"Save ordered visible report dimension chips globally for the current user across workspaces and sessions. User sessions and OAuth users only; API keys are rejected. Empty arrays hide all chips. This does not modify timezone or workspace presets. Returns {visible_chips}.","inputSchema":{"type":"object","properties":{"visible_chips":{"type":"array","items":{"type":"string","enum":["link","ref","meta_campaign","meta_adset","meta_ad","day","country","region","city","continent","timezone","postal","colo","device","os","browser","os_version","browser_version","device_vendor","device_model","isp","asn","language","http_protocol","tls_version","suspicious","lander","offer","stage","event","network","lander_node","utm_source","utm_medium","utm_campaign","utm_content","utm_term","utm_placement","placement","publisher"]},"maxItems":40,"uniqueItems":true}},"required":["visible_chips"],"additionalProperties":false}},{"name":"report_breakdown","title":"Breakdown report","category":"reporting","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /report/breakdown"],"description":"THE reporting query: GROUP BY up to 4 dimension(s) over a UTC time window. report_timezone selects IANA calendar-day grouping for visits, leads, conversions and cost; it defaults to the user’s saved timezone for sessions/OAuth, UTC for API keys. An explicit report_timezone overrides that default and is returned in the response. It is independent of the timezone dimension, which describes the visitor. Dims: link, ref, meta_campaign, meta_adset, meta_ad, day, country, region, city, continent, timezone, postal, colo, device, os, browser, os_version, browser_version, device_vendor, device_model, isp, asn, language, http_protocol, tls_version, suspicious, lander, offer, stage, event, network, lander_node, utm_source, utm_medium, utm_campaign, utm_content, utm_term, utm_placement, placement, publisher. Returns per group: visits, uniq_visits, clicks, uniq_clicks, leads, conversions, revenue, suspicious_visits, ttc_sum_s (summed visit→conversion seconds; avg time-to-conversion = ttc_sum_s/conversions) and cost for every dimension chain. Also returns top-level unattributed_cost_usd: window-total Meta spend whose Meta campaign has no RC mapping, excluded from every row/total above and reported separately so it stays visible. Cost combines Meta spend and configured flat CPC. Meta spend uses the selected report day and creative ids; split dimensions use stage-0 visit shares for the captured ad, falling back to all visits of its mapped campaign in the window. Shares are calculated before filters. Offer cost belongs to the saved stage-0 monetization offer, else the destination offer, never both. No-visit spend stays in cost-only rows with unknown visit dimensions when its Meta campaign is mapped; otherwise it is unattributed, not a link=null row. Mapping a campaign later attributes its historical spend on the next call, no reimport or duplication. Event cost stays under event=null, not each conversion event. Network groups/filters use the effective offer for each fact: destination for visits and exact verified leads, saved attributed offer for conversions, stage-0 monetization fallback for cost. Network values are workspace-owned network IDs (including exact legacy-name matches), otherwise \"legacy:\" plus the offer network label (or \"unassigned\"), replacing % with %25 then comma with %2C. Missing/deleted offers have network=null; filtering a network with no offers returns no facts. CTR=clicks/visits, CVR=conversions/visits, EPC=revenue/clicks, ROI=(revenue-cost)/cost, ROAS=revenue/cost. Dashboard/campaign/report ROAS displays a two-decimal x multiplier (dash if cost=0); CSV roas is numeric. ROAS is green above 1x, red below 1x, neutral at 1x or undefined zero spend, including report totals/nested groups. Reporting-table uEPV=revenue/uniq_visits and uEPC=revenue/uniq_clicks (0 if the unique denominator is zero or missing). Dashboard CPL=cost/leads; iCPL=(cost-revenue)/leads (negative means revenue exceeds cost). Both display dash and export blank CSV cells when leads=0; totals divide summed cost/net cost by summed leads. REST/MCP callers derive these metrics from returned cost, revenue and leads. Unique counts are per-group hashed IPs; summing groups does not deduplicate across groups. Rows sort by revenue desc (max 1000). Multi-page funnels record one visit per page-serve (same click_id, stage 0..N) — visits = page views; dims=[\"stage\"] is the drop-off report. Clicks anchor to stage 0; conversions use their saved attributed stage/offer/lander, falling back to stage 0 for legacy callbacks (never multiplied by pages). Leads count only submissions accepted by RC /lead/:linkId, never external opt-in handoffs or flow_lead pixels. Lead windows/day buckets use submission time, unlike conversions’ attributed visit time. Verified lead context supplies lander/stage and the exact matching visit supplies offer/visitor dimensions; historical or unsigned leads retain link/ref without borrowing page attribution. Leads never multiply visits, clicks, conversions or cost. dims=[\"event\"] slices conversions by /pb event name. Filters accept string or real null values, including day in report_timezone; commas and literal dashes are preserved. paths is an OR of exact complete row paths (parents included), ANDed with ordinary filters. Never replace paths with independent dimension lists, which would admit unwanted cross-products. Attribution UTMs, placement and publisher use captured role mappings, not today’s traffic source config. Slot t1..t20 and campaign ct1..ct10 are omitted because historical slot order/values were not persisted.","inputSchema":{"type":"object","properties":{"dims":{"type":"array","items":{"type":"string","enum":["link","ref","meta_campaign","meta_adset","meta_ad","day","country","region","city","continent","timezone","postal","colo","device","os","browser","os_version","browser_version","device_vendor","device_model","isp","asn","language","http_protocol","tls_version","suspicious","lander","offer","stage","event","network","lander_node","utm_source","utm_medium","utm_campaign","utm_content","utm_term","utm_placement","placement","publisher"]},"minItems":1,"maxItems":4,"uniqueItems":true,"description":"Ordered dimension chain"},"from":{"type":"string","description":"Window start, ISO-8601 UTC (default: 7 days ago)"},"to":{"type":"string","description":"Window end, ISO-8601 UTC exclusive (default: now)"},"report_timezone":{"type":"string","description":"UTC or IANA Area/Location for calendar-day grouping; no abbreviations (default: saved user timezone, UTC for keys)"},"filters":{"type":"object","additionalProperties":false,"properties":{"link":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"ref":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"meta_campaign":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"meta_adset":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"meta_ad":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"day":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"country":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"region":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"city":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"continent":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"timezone":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"postal":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"colo":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"device":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"os":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"browser":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"os_version":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"browser_version":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"device_vendor":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"device_model":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"isp":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"asn":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"language":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"http_protocol":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"tls_version":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"suspicious":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"lander":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"offer":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"stage":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"event":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"network":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"lander_node":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"utm_source":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"utm_medium":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"utm_campaign":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"utm_content":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"utm_term":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"utm_placement":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"placement":{"type":"array","items":{"type":["string","null"]},"maxItems":200},"publisher":{"type":"array","items":{"type":["string","null"]},"maxItems":200}},"description":"ANDed dimension filters; each array ORs exact strings/null (up to 200 values)"},"paths":{"type":"array","items":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"link":{"type":["string","null"]},"ref":{"type":["string","null"]},"meta_campaign":{"type":["string","null"]},"meta_adset":{"type":["string","null"]},"meta_ad":{"type":["string","null"]},"day":{"type":["string","null"]},"country":{"type":["string","null"]},"region":{"type":["string","null"]},"city":{"type":["string","null"]},"continent":{"type":["string","null"]},"timezone":{"type":["string","null"]},"postal":{"type":["string","null"]},"colo":{"type":["string","null"]},"device":{"type":["string","null"]},"os":{"type":["string","null"]},"browser":{"type":["string","null"]},"os_version":{"type":["string","null"]},"browser_version":{"type":["string","null"]},"device_vendor":{"type":["string","null"]},"device_model":{"type":["string","null"]},"isp":{"type":["string","null"]},"asn":{"type":["string","null"]},"language":{"type":["string","null"]},"http_protocol":{"type":["string","null"]},"tls_version":{"type":["string","null"]},"suspicious":{"type":["string","null"]},"lander":{"type":["string","null"]},"offer":{"type":["string","null"]},"stage":{"type":["string","null"]},"event":{"type":["string","null"]},"network":{"type":["string","null"]},"lander_node":{"type":["string","null"]},"utm_source":{"type":["string","null"]},"utm_medium":{"type":["string","null"]},"utm_campaign":{"type":["string","null"]},"utm_content":{"type":["string","null"]},"utm_term":{"type":["string","null"]},"utm_placement":{"type":["string","null"]},"placement":{"type":["string","null"]},"publisher":{"type":["string","null"]}}},"minItems":1,"maxItems":50,"description":"OR of complete selected row paths, including parents; ANDed with filters"}},"required":["dims"],"additionalProperties":false}},{"name":"report_values","title":"Dimension values","category":"reporting","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /report/values"],"description":"Distinct values of one dimension seen in-range (default 100, max 200 per page) — use to discover valid filter values for report_breakdown. Offer and network values include saved stage-0 monetization associations even before their first conversion. Network values also include approved conversions’ saved attributed offers and use the same canonical ID/encoded legacy keys as breakdown. Accepted RC leads contribute values in their submission-time window, even when their verified visit was earlier. Lead visitor/offer/network values use only the exact verified page; historical or unsigned leads contribute link/ref without invented lander/stage values. Day values use report_timezone. q is a case-insensitive literal substring (not SQL wildcards), offset pages a deterministic frequency/value order. Returns a plain array of strings/null. Manual exact filter values may also be supplied without discovery.","inputSchema":{"type":"object","properties":{"dim":{"type":"string","enum":["link","ref","meta_campaign","meta_adset","meta_ad","day","country","region","city","continent","timezone","postal","colo","device","os","browser","os_version","browser_version","device_vendor","device_model","isp","asn","language","http_protocol","tls_version","suspicious","lander","offer","stage","event","network","lander_node","utm_source","utm_medium","utm_campaign","utm_content","utm_term","utm_placement","placement","publisher"],"description":"Dimension"},"from":{"type":"string","description":"Window start ISO-8601 (default: 30 days ago)"},"to":{"type":"string","description":"Window end ISO-8601 exclusive (default: now)"},"report_timezone":{"type":"string","description":"UTC or IANA Area/Location; default saved user timezone, UTC for keys"},"q":{"type":"string","description":"Literal case-insensitive substring","maxLength":300},"limit":{"type":"integer","description":"Page size (default 100)","minimum":1,"maximum":200},"offset":{"type":"integer","description":"Page offset (default 0)","minimum":0,"maximum":100000}},"required":["dim"],"additionalProperties":false}},{"name":"report_filtered","title":"Filtered visits","category":"reporting","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /report/filtered"],"description":"How many visitors the Filter nodes on each campaign turned away, grouped by campaign and filter node, for a date window (UTC days). Returns report_timezone=\"UTC\" and granularity=\"day\": these historical counters cannot be re-bucketed to a user timezone. A Filter node returns a bare 404 and records no visit, click or cost, so these counts exist nowhere else — they are NOT part of report_breakdown, which is computed from visits. Read the hits for a campaign beside its visit count for the same window to judge whether a rule is over-firing. node_id is the flow-graph node id of the filter (see routing.graph on the campaign) — a campaign edited to remove a filter keeps the history of what it did. Counts are an indicator of scale, not an exact ledger: queue delivery is at-least-once, so a retry can double-count.","inputSchema":{"type":"object","properties":{"from":{"type":"string","description":"Window start, ISO-8601 or YYYY-MM-DD (default: 7 days ago)"},"to":{"type":"string","description":"Window end, ISO-8601 or YYYY-MM-DD, inclusive (default: today)"},"link_id":{"type":"string","description":"Limit to one campaign (link id)"}},"additionalProperties":false}},{"name":"list_conversion_events","title":"List conversion events","category":"reporting","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /conversion-events"],"description":"List workspace conversion event definitions: id, immutable name, description, created_at. Inbound event names are exact and case-sensitive; unregistered names still appear in reports.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_conversion_event","title":"Create conversion event","category":"reporting","scope":"conversion_events:write","readOnly":false,"destructive":false,"covers":["POST /conversion-events"],"description":"Define a workspace conversion event name and optional description. Send this exact name as event= in inbound postbacks; it labels conversions without changing revenue.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Immutable, case-sensitive name","minLength":1,"maxLength":100},"description":{"type":"string","description":"Optional description","maxLength":1000}},"required":["name"],"additionalProperties":false}},{"name":"update_conversion_event","title":"Update conversion event","category":"reporting","scope":"conversion_events:write","readOnly":false,"destructive":false,"covers":["PATCH /conversion-events/:id"],"description":"Update a conversion event description. Names are immutable so historical classifications and saved pixel filters cannot silently change.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Conversion event id (ce_…)"},"description":{"type":"string","description":"Description; empty clears it","maxLength":1000}},"required":["id","description"],"additionalProperties":false}},{"name":"delete_conversion_event","title":"Delete conversion event","category":"reporting","scope":"conversion_events:write","readOnly":false,"destructive":true,"covers":["DELETE /conversion-events/:id"],"description":"Delete a workspace event definition only. Historical conversions and saved pixel filter names remain unchanged; inbound postbacks with this name are still accepted.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Conversion event id (ce_…)"}},"required":["id"],"additionalProperties":false}},{"name":"list_conversions","title":"List conversions","category":"reporting","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /conversions"],"description":"Paginated workspace-owned conversion/postback records with full stored attribution, event, raw parameters and exact attributed-visit context. Filter event by exact conversion event name. Arbitrary raw/identity fields require additional reports:identity permission (owner by default); others receive a safe projection. unmatched never exposes globally unowned historical rows. Request bodies load via get_event_detail. Response event_names lists names across all statuses in the same conversion-time window and campaign, before event filtering/pagination. Change status via bulk_action on \"conversions\".","inputSchema":{"type":"object","properties":{"from":{"type":"string","description":"Window start ISO-8601 (default: 7 days ago)"},"to":{"type":"string","description":"Window end ISO-8601 (default: now)"},"link":{"type":"string","description":"Filter to one campaign id"},"event":{"type":"string","description":"Exact, case-sensitive conversion event name"},"unmatched":{"type":"boolean","description":"Accepted for compatibility; never includes globally unowned historical rows"},"limit":{"type":"integer","description":"Page size, max 200 (default 50)"},"offset":{"type":"integer","description":"Pagination offset"}},"additionalProperties":false}},{"name":"get_event_detail","title":"Inspect event and request","category":"reporting","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /events/:kind/:id"],"description":"Full workspace-owned view/click/conversion/postback record and actual retained request snapshot, plus up to 100 same-workspace related visits (explicit truncated marker). No invented historical bodies: request=null means unavailable. Credentials are always redacted. Additional reports:identity permission is required for arbitrary bodies, raw params and identity context; report-only actors receive a safe projection. Use record_id from list_events/list_conversions.","inputSchema":{"type":"object","properties":{"kind":{"type":"string","enum":["view","click","conversion","postback"]},"id":{"type":"string","description":"Stable record_id from an events/conversions list"}},"required":["kind","id"],"additionalProperties":false}},{"name":"list_errors","title":"List delivery errors","category":"reporting","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /errors"],"description":"Unified failed-delivery feed, newest first: failed S2S conversion feedback to traffic sources (kind=network) and failed pixel/CAPI fires (kind=pixel). Each row: kind, ts, name (source/pixel), conversion_id, event_name, response_status, detail. The one-look \"is anything broken\" check; per-source/per-pixel logs remain on their own entities.","inputSchema":{"type":"object","properties":{"since":{"type":"string","description":"Only errors at/after this ISO-8601 instant (default: 7 days ago)"},"limit":{"type":"integer","description":"Max rows, cap 500 (default 200)"}},"additionalProperties":false}},{"name":"get_stack_health","title":"Get stack health","category":"reporting","scope":"ops:read","readOnly":true,"destructive":false,"covers":["GET /ops/health"],"description":"Federated health of the whole BULLPRESS stack — every background sync, job and worker with status (ok/stale/degraded/down/idle/unknown), last-run age, and failure detail, plus per-service up/down. Collected and judged by rp-edge (the system of record); RoninConv only relays it, so {unavailable:true, kind, reason, detail} means no report was obtained, which is itself the answer. kind is \"not_configured\" (this worker has no collector URL/secret), \"unreachable\" (rp-edge did not answer) or \"bad_response\" (it answered, but not with a health report: wrong URL, or a rejected secret); reason says what to check. Owner-only (ops:read): global infrastructure state, not workspace reporting data. ops:read is explicit-opt-in: the \"full\" key scope does not include it.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_events","title":"List events","category":"reporting","scope":"reports:read","readOnly":true,"destructive":false,"covers":["GET /events"],"description":"Merged raw event timeline — view (lander serve), click (CTA), conversion — with geo/device columns, paginated. The forensic view: filter by campaign, click_id, event type, conversion_event (exact classification name), device, os, country, browser. event remains the timeline type; conversion_event is the inbound conversion name or null. Includes full stored IDs, timing, geo, device, params, attribution/context and request availability, not request bodies. Additional reports:identity permission enables arbitrary parameters and identity; otherwise a safe projection is returned. Click context is explicitly the latest stored visit before click, not exact attribution. Response event_names lists all nonnull conversion names for the same timeline population, before conversion_event filtering/pagination.","inputSchema":{"type":"object","properties":{"from":{"type":"string","description":"Window start ISO-8601 (default: 24h ago)"},"to":{"type":"string","description":"Window end ISO-8601 (default: now)"},"link":{"type":"string","description":"Filter to one campaign id"},"click_id":{"type":"string","description":"Follow one visitor journey by click id"},"type":{"type":"string","enum":["view","click","conversion"],"description":"Event type filter"},"conversion_event":{"type":"string","description":"Exact, case-sensitive conversion event name"},"device":{"type":"string","description":"Device filter (mobile|desktop|tablet)"},"os":{"type":"string","description":"OS filter"},"country":{"type":"string","description":"Country filter (ISO-3166 alpha-2)"},"browser":{"type":"string","description":"Browser filter"},"limit":{"type":"integer","description":"Page size, max 200 (default 50)"},"offset":{"type":"integer","description":"Pagination offset"}},"additionalProperties":false}},{"name":"list_webhooks","title":"List webhooks","category":"webhooks","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /webhooks"],"description":"List outbound webhook subscriptions (HMAC-SHA256-signed event fan-out to your own endpoints).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_webhook","title":"Create webhook","category":"webhooks","scope":"webhooks:manage","readOnly":false,"destructive":false,"covers":["POST /webhooks"],"description":"Subscribe a URL to workspace events (default [\"conversion\"]). Returns the signing secret ONCE — deliveries carry an HMAC-SHA256 signature of the body with it.","inputSchema":{"type":"object","properties":{"url":{"type":"string","description":"HTTPS endpoint to receive event POSTs"},"events":{"type":"array","items":{"type":"string","enum":["conversion","click","visit"]},"description":"Event types (default [\"conversion\"])"}},"required":["url"],"additionalProperties":false}},{"name":"delete_webhook","title":"Delete webhook","category":"webhooks","scope":"webhooks:manage","readOnly":false,"destructive":true,"covers":["DELETE /webhooks/:id"],"description":"Remove a webhook subscription. Deliveries stop immediately; the delivery history in the Errors feed is retained.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Webhook subscription id"}},"required":["id"],"additionalProperties":false}},{"name":"test_webhook","title":"Test webhook","category":"webhooks","scope":"webhooks:manage","readOnly":false,"destructive":false,"covers":["POST /webhooks/:id/test"],"description":"Send one synthetic event to a subscription so the receiving workflow can be built against a real payload before any traffic exists. Signed exactly like a live delivery, and flagged with test:true in the body. Returns the status and response body.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Webhook subscription id"}},"required":["id"],"additionalProperties":false}},{"name":"get_postback_info","title":"Get postback info","category":"webhooks","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /postback-info"],"description":"Valid router hostnames, per-network secrets (select the exact network ID), ready/needs_secret flags, and a GENERIC URL template; there is no top-level/shared secret. Secrets and usable templates are withheld until exact scoped KV readback succeeds. /pb?secret=…&click_id=…&payout=…&txid=…&event=…&status=… is not an Everflow callback. For Everflow use everflow_register_postback, which requires the network’s own secret and returns offer_id={offer_id}, provider_event_id={event_id}, txid={transaction_id}, payout={payout_amount} plus the configured click/contact/hash macros. Manually replace existing callbacks rather than adding duplicates; missing secrets require initialize_network_postback_secret before installation.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"get_n8n_connection","title":"Get n8n connection","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /n8n"],"description":"The workspace n8n connection: base_url, connected (bool), token_last4. The API key itself is never returned.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"set_n8n_connection","title":"Set n8n connection","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /n8n"],"description":"Set the n8n base URL and/or API key (Settings → n8n API in n8n). Omitting token keeps the stored one, so the URL can be edited without re-entering the key; sending an empty token disconnects. Test it with test_n8n_connection.","inputSchema":{"type":"object","properties":{"base_url":{"type":"string","description":"https://… root of the n8n instance"},"token":{"type":"string","description":"n8n API key; omit to keep, \"\" to clear"}},"additionalProperties":false}},{"name":"test_n8n_connection","title":"Test n8n connection","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /n8n/test"],"description":"Live call against the n8n API. Distinguishes a bad key (401) from an unreachable or wrong URL — check this before relying on any other n8n tool.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_n8n_workflows","title":"List n8n workflows","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /n8n/workflows"],"description":"Workflows on the connected n8n instance: id, name, active, webhook_path, node count. This is the menu to adopt from — pass an id to adopt_n8n_template.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_n8n_templates","title":"List n8n templates","category":"settings","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /n8n/templates"],"description":"Workflow templates adopted into this workspace: id, name, the n8n workflow each came from, and their variables (with the nodes/fields each one controls).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"adopt_n8n_template","title":"Adopt n8n workflow as template","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /n8n/templates"],"description":"Copy a live n8n workflow into RoninConv as a reusable template. Variables are auto-detected (webhook path, workflow name, allowed origin — plus every other place those same strings appear, e.g. downstream source tags), and returned so they can be renamed or trimmed with update_n8n_template. Adopting never modifies the source workflow.","inputSchema":{"type":"object","properties":{"workflow_id":{"type":"string","description":"n8n workflow id (see list_n8n_workflows)"},"name":{"type":"string","description":"Template name (default: the workflow name)"},"variables":{"type":"array","items":{"type":"object","description":"{key,label,value} — overrides auto-detection"},"description":"Explicit variables, optional"}},"required":["workflow_id"],"additionalProperties":false}},{"name":"find_n8n_variables","title":"Find template variables","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /n8n/templates/:id/candidates"],"description":"Locate a value so it can be made editable per campaign. Auto-detection already covers the webhook path, workflow name and origin; this is for everything else (a CRM custom-field id, a Slack label, a tag). NORMAL PATH: pass `q` with the exact value — it reports whether it exists and which nodes carry it, with no relevance filtering, because whoever is promoting a value already knows it. Fallbacks when the value is unknown: omit everything to list all configurable literals ranked by node reach, or pass `against` (an n8n workflow id) to diff two copies — what differs IS the variable set. Promote with update_n8n_template.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Template id"},"q":{"type":"string","description":"Exact value to locate — the normal way to use this"},"against":{"type":"string","description":"n8n workflow id to diff against, when the value is unknown"}},"required":["id"],"additionalProperties":false}},{"name":"update_n8n_template","title":"Update n8n template","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["PATCH /n8n/templates/:id"],"description":"Rename a template or replace its variable list (e.g. drop a detected value that should stay constant across copies, or add one detection missed).","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Template id"},"name":{"type":"string","description":"New name"},"variables":{"type":"array","items":{"type":"object","description":"{key,label,value}"},"description":"Replacement variables"}},"required":["id"],"additionalProperties":false}},{"name":"delete_n8n_template","title":"Delete n8n template","category":"settings","scope":"links:write","readOnly":false,"destructive":true,"covers":["DELETE /n8n/templates/:id"],"description":"Delete a template from RoninConv. The n8n workflow it was adopted from is untouched.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Template id"}},"required":["id"],"additionalProperties":false}},{"name":"create_n8n_workflow","title":"Create workflow from template","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /n8n/templates/:id/create"],"description":"Create a real workflow in n8n from a template, substituting the variable values given. Always created INACTIVE — activate in n8n after review. Each value must be 3+ chars (shorter would match inside unrelated words during substitution), and the call fails 409 if any old value survives substitution rather than shipping a half-renamed workflow. A value may change literal text, never code: it is refused 400 if it carries n8n expression syntax, or if its variable lands inside a code-bearing field (jsCode, pythonCode, query, html, or a node's type), which n8n runs verbatim with $env in scope. Pass dry_run to see what would change without creating. link_id also binds the new workflow to that campaign (its webhook path is what rp-edge routes leads by).","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Template id"},"values":{"type":"object","description":"{variable_key: new value} — see list_n8n_templates for the keys"},"name":{"type":"string","description":"Name for the new workflow"},"link_id":{"type":"string","description":"Campaign id to bind the new workflow to, optional"},"dry_run":{"type":"boolean","description":"Report the substitutions without creating anything"}},"required":["id","values"],"additionalProperties":false}},{"name":"get_n8n_step_catalog","title":"Get workflow step catalog","category":"settings","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /n8n/catalog"],"description":"The step kinds a blueprint can be built from, with their fields. Deliberately a short list covering the repetitive per-campaign shapes (receive, filter, wait, forward, notify, reply) — anything more elaborate should be built in n8n and adopted as a template instead (see list_n8n_templates).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_n8n_blueprints","title":"List workflow blueprints","category":"settings","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /n8n/blueprints"],"description":"Workflows composed step-by-step in RoninConv, with their steps, the n8n workflow id once pushed, and the webhook path. The steps are the source of truth; the workflow in n8n is rebuilt from them on every push.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_n8n_blueprint","title":"Create workflow blueprint","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /n8n/blueprints"],"description":"Compose a workflow as an ordered chain of steps. The first step must be a trigger. Steps are validated on save, so an invalid blueprint cannot be stored and then fail later at push time. Creating a blueprint does not touch n8n — push it separately.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Workflow name"},"steps":{"type":"array","items":{"type":"object"},"description":"Ordered steps; each {kind, ...fields} — see get_n8n_step_catalog"}},"required":["name"],"additionalProperties":false}},{"name":"update_n8n_blueprint","title":"Update workflow blueprint","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /n8n/blueprints/:id"],"description":"Rename a blueprint or replace its steps. Does not touch n8n until pushed.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Blueprint id"},"name":{"type":"string","description":"New name"},"steps":{"type":"array","items":{"type":"object"},"description":"Replacement steps"}},"required":["id"],"additionalProperties":false}},{"name":"delete_n8n_blueprint","title":"Delete workflow blueprint","category":"settings","scope":"links:write","readOnly":false,"destructive":true,"covers":["DELETE /n8n/blueprints/:id"],"description":"Remove a blueprint. The workflow it created in n8n is deliberately LEFT ALONE — it may be live and taking traffic, and deleting a blueprint should never silently stop it.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Blueprint id"}},"required":["id"],"additionalProperties":false}},{"name":"push_n8n_blueprint","title":"Push blueprint to n8n","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /n8n/blueprints/:id/push"],"description":"Build the workflow from the blueprint and send it to n8n. Always INACTIVE — activate in n8n after review. Pushing again UPDATES the same workflow rather than making a near-duplicate, which also means any edit made inside n8n is overwritten. Pass dry_run to get the generated workflow JSON without sending it. link_id binds the workflow to that campaign (its webhook path is what rp-edge routes leads by); an unknown link_id is a 404 and nothing is pushed.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Blueprint id"},"link_id":{"type":"string","description":"Campaign id to bind the workflow to, optional"},"dry_run":{"type":"boolean","description":"Return the generated workflow without sending it"}},"required":["id"],"additionalProperties":false}},{"name":"set_campaign_n8n","title":"Link campaign to n8n workflow","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /links/:id/n8n"],"description":"Record which n8n workflow serves a campaign's leads. webhook_path is the operationally meaningful half — rp-edge routes each campaign's lead delivery by that value — while workflow_id is for deep-linking the n8n editor. Send null/\"\" to unlink.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Campaign id"},"webhook_path":{"type":"string","description":"n8n webhook path, e.g. \"bh-hft\""},"workflow_id":{"type":"string","description":"n8n workflow id"}},"required":["id"],"additionalProperties":false}},{"name":"set_link_advertiser","title":"Link campaign to advertiser network","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /links/:id/advertiser"],"description":"Tie a campaign to its advertiser network record (the network holds the advertiser's revshare_pct/attribution_window_days defaults, see update_network). Send null/\"\" to unlink. 400 if the network id does not exist in this workspace.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Campaign id"},"advertiser_network_id":{"type":"string","description":"Network id (nw_…); null/\"\" to unlink"}},"required":["id"],"additionalProperties":false}},{"name":"add_meta_campaign_id","title":"Add Meta campaign id to a campaign","category":"campaigns","scope":"sentry:write","readOnly":false,"destructive":false,"covers":["POST /links/meta-campaign-ids"],"description":"Atomically append a Meta campaign id to an active RoninConv campaign, resolved by ref (deduplicates; returns canonical meta_campaign_ids). Many Meta ids may back one RC campaign, but each id belongs to at most one RC campaign per workspace; conflicting ownership returns 409. IDs may precede catalog discovery. The same normalized mapping drives UI, API and report cost; sync imports account-wide ad-hour spend, not a per-link list. This is SENTRY hold-approval write-back. sentry:write is explicit-opt-in: the \"full\" key scope does not include it.","inputSchema":{"type":"object","properties":{"ref":{"type":"string","description":"Campaign ref to match against active links"},"meta_campaign_id":{"type":"string","description":"Meta campaign id to add"}},"required":["ref","meta_campaign_id"],"additionalProperties":false}},{"name":"list_value_lists","title":"List value lists","category":"campaigns","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /value-lists"],"description":"Named, reusable value sets referenced by rule conditions (the equivalent of a tracker's black/white lists, e.g. \"FB MMA CampaignIDs\"): id, name, values, created_at. Reference one from a param/token condition as {\"list_id\":\"vl_…\"} instead of inline values.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"everflow_import_creatives","title":"Import Everflow creatives","category":"offers","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /networks/:id/everflow/creatives/import"],"description":"Pull an offer's creatives from Everflow into its creative store, so agents read copy from the API instead of the network UI. Idempotent on Everflow's own creative id: re-running updates the rows it wrote rather than duplicating them. One Everflow creative can become several rows: an email creative yields its body plus one row per subject line and from line, each separately addressable. Paused creatives are skipped. Rows an earlier import wrote that this pull no longer contains come back as `stale`, and are never deleted for you. Pass dry_run to see exactly what would be written without writing it.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Network id"},"offer_id":{"type":"string","description":"RoninConv offer id to attach the creatives to"},"network_offer_id":{"type":"string","description":"Everflow offer id. Defaults to the offer's external_id"},"dry_run":{"type":"boolean","description":"Preview without writing"}},"required":["id","offer_id"],"additionalProperties":false}},{"name":"list_offer_creatives","title":"List offer creatives","category":"campaigns","scope":"entities:read","readOnly":false,"destructive":false,"covers":["GET /offers/:id/creatives"],"description":"Every piece of copy and creative attached to an offer: email subject lines and bodies, SMS copy, headlines, CTAs, disclaimers, and urls for image and video assets. This is the one place to read offer creative from, so there is no need to sign into the network UI of the advertiser to get it. Ordered by position, then id, so a set of subject lines comes back the way it was written. Optionally filter to one kind.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Offer id"},"kind":{"type":"string","description":"Only this kind, e.g. email_subject | email_body | sms | headline | cta | disclaimer | image | video | note"}},"required":["id"],"additionalProperties":false}},{"name":"add_offer_creative","title":"Add offer creative","category":"campaigns","scope":"offers:write","readOnly":false,"destructive":false,"covers":["POST /offers/:id/creatives"],"description":"Attach one piece of copy or creative to an offer. `kind` is free text. The conventional set is email_subject, email_body, sms, headline, cta, disclaimer, image, video, note, so a format the networks invent later needs no migration. Text goes in `body`; images and video are referenced by `url` rather than stored. A url must be http(s), because these are read back by agents writing landers and emails.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Offer id"},"kind":{"type":"string","description":"What this is, e.g. email_subject | email_body | sms | headline | cta | disclaimer | image | video | note"},"label":{"type":"string","description":"Short human label, e.g. \"Variant B\" or \"hero image\""},"body":{"type":"string","description":"The copy itself. Empty for an image or video, which carry a url instead"},"url":{"type":"string","description":"Absolute http(s) url, for an image, video, or anything whose source is worth keeping"},"position":{"type":"number","description":"Sort order within the offer, lowest first. Defaults to 0"}},"required":["id","kind"],"additionalProperties":false}},{"name":"update_offer_creative","title":"Update offer creative","category":"campaigns","scope":"offers:write","readOnly":false,"destructive":false,"covers":["PATCH /creatives/:id"],"description":"Edit one creative in place. Only the fields you pass change; the rest keep their current values. The creative stays on the offer it was added to.","inputSchema":{"type":"object","properties":{"id":{"type":"number","description":"Creative id, from list_offer_creatives"},"kind":{"type":"string","description":"New kind"},"label":{"type":"string","description":"New label"},"body":{"type":"string","description":"Replacement copy"},"url":{"type":"string","description":"Replacement absolute http(s) url, or empty string to clear it"},"position":{"type":"number","description":"New sort order"}},"required":["id"],"additionalProperties":false}},{"name":"delete_offer_creative","title":"Delete offer creative","category":"campaigns","scope":"offers:write","readOnly":false,"destructive":true,"covers":["DELETE /creatives/:id"],"description":"Remove one creative. Deleting the offer itself removes its creatives anyway, so this is for pruning a single variant rather than cleaning up after an offer.","inputSchema":{"type":"object","properties":{"id":{"type":"number","description":"Creative id, from list_offer_creatives"}},"required":["id"],"additionalProperties":false}},{"name":"create_value_list","title":"Create value list","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /value-lists"],"description":"Create a named value list (max 5000 values). Editing it later re-syncs every campaign whose flow cites a rule referencing it, so the whitelist lives in one place instead of being copied into each rule.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"List name, e.g. \"FB MMA CampaignIDs\""},"values":{"type":"array","items":{"type":"string","description":"A value"},"description":"Values"}},"required":["name"],"additionalProperties":false}},{"name":"update_value_list","title":"Update value list","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["PATCH /value-lists/:id"],"description":"Rename a list or replace its values (full replace, not a merge). Re-syncs every campaign that routes on it within seconds.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"List id"},"name":{"type":"string","description":"New name"},"values":{"type":"array","items":{"type":"string","description":"A value"},"description":"Replacement values"}},"required":["id"],"additionalProperties":false}},{"name":"delete_value_list","title":"Delete value list","category":"campaigns","scope":"links:write","readOnly":false,"destructive":true,"covers":["DELETE /value-lists/:id"],"description":"Delete a value list. Refused (409) while any rule still references it — the error names the rules.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"List id"}},"required":["id"],"additionalProperties":false}},{"name":"list_rules","title":"List routing rules","category":"campaigns","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /rules"],"description":"Named, reusable routing predicates referenced by flow edges as when:{\"rule\":\"rl_…\"}. Each: id, name, logical (and|or), conds[]. Every condition takes `not` to invert. Types:\n• country/device/os — {type,values:[…]}\n• suspicious — the roll-up: ANY traffic rule fired\n• bot_ua / datacenter / moderator — the individual bot signals, routable separately so \"any bot signal OR an unsubstituted macro\" is expressible in one rule. moderator uses the operator-supplied moderator_asns/moderator_ips in traffic settings (empty = never fires).\n• param — {type:\"param\",key,op:equals|contains|exists,value?|values?[]|list_id?,include_empty?}: matches a raw ad-URL query value.\n• token — same, but {slot:1-20} instead of key: resolves through the traffic source's t1..t20 map, so the rule survives a parameter rename.\n• role — same, but {role:\"utm_campaign\"|\"email_hash\"|…} instead of key or slot: resolves through the token carrying that role. PREFER THIS over `token`: a slot number changes meaning if a slot is inserted or removed on the source, a role does not. See set_templates for the vocabulary.\ninclude_empty is OFF by default: an absent value is not treated as a match, because an unset macro is not the same as one that resolved to the tested value. list_id references a value list (see list_value_lists) instead of inlining values.\nIdiom worth knowing: testing a token against its own literal macro (e.g. token 6 equals \"{{ad.id}}\") detects an UNSUBSTITUTED macro — the platform only leaves it raw for crawlers and reviewers, so it is a reliable bot signal.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_rule","title":"Create routing rule","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /rules"],"description":"Create a reusable rule. Reference it from a campaign flow edge or path node as when:{\"rule\":\"<id>\"} (via create/update_campaign routing). Editing the rule live-resyncs every campaign that references it. logical combines the conds (and|or).","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Display name, e.g. \"isFBBot\" or \"US only\""},"logical":{"type":"string","enum":["and","or"],"description":"How conds combine (default and)"},"conds":{"type":"array","items":{"type":"object","description":"A condition (see list_rules for the shapes)"},"description":"Condition list (max 20)"}},"required":["name","conds"],"additionalProperties":false}},{"name":"update_rule","title":"Update routing rule","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["PATCH /rules/:id"],"description":"Patch a rule (name/logical/conds; omitted fields kept). Re-syncs every campaign referencing it within seconds.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Rule id"},"name":{"type":"string","description":"New name"},"logical":{"type":"string","enum":["and","or"]},"conds":{"type":"array","items":{"type":"object","description":"Condition"},"description":"Replacement condition list"}},"required":["id"],"additionalProperties":false}},{"name":"delete_rule","title":"Delete routing rule","category":"campaigns","scope":"links:write","readOnly":false,"destructive":true,"covers":["DELETE /rules/:id"],"description":"Delete a rule. Refused (409) if any campaign still references it — remove it from their flows first; the error lists the campaign ids.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Rule id"}},"required":["id"],"additionalProperties":false}},{"name":"get_traffic_settings","title":"Get traffic settings","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /traffic-settings"],"description":"Current workspace bot-filtering config enforced by the edge router: enabled, mode (mark|block), UA rules, ASN/IP/country lists, per-IP rate limit — plus lead_endpoint (the lead-capture URL injected into served landers as __rc.lead_endpoint, or the 'rc-internal' sentinel).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"update_traffic_settings","title":"Update traffic settings","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /traffic-settings"],"description":"Replace the workspace bot-filtering config (full PUT — send every field you want kept; unspecified lists reset). mode \"mark\" serves the lander but flags the visit suspicious; \"block\" returns 403 and logs nothing. Live at the edge within seconds. Also carries lead_endpoint: the workspace lead-capture URL injected into every served lander's window.__rc.lead_endpoint (https only, or the 'rc-internal' sentinel to route leads through RC's own capture first; empty clears it).","inputSchema":{"type":"object","properties":{"lead_endpoint":{"type":"string","description":"Lead-capture endpoint: an https://… URL, the sentinel 'rc-internal' (routes through RC first, then forwards to rp-edge), or empty for none"},"enabled":{"type":"boolean","description":"Master switch"},"mode":{"type":"string","enum":["mark","block"],"description":"What a rule hit does"},"block_empty_ua":{"type":"boolean","description":"Flag requests with no User-Agent (default true)"},"builtin_bot_ua":{"type":"boolean","description":"Use the built-in bot UA list — curl/python/headless/spiders (default true)"},"ua_patterns":{"type":"array","items":{"type":"string","description":"regex"},"description":"Custom UA block patterns (max 50)"},"asn_block":{"type":"array","items":{"type":"string","description":"ASN number as string, e.g. \"16509\""},"description":"Blocked ASNs (max 200)"},"ip_block":{"type":"array","items":{"type":"string","description":"IP or CIDR, e.g. \"5.6.0.0/16\""},"description":"Blocked IPs/CIDRs (max 500)"},"country_block":{"type":"array","items":{"type":"string","description":"ISO code"},"description":"Blocked countries (max 100)"},"country_allow":{"type":"array","items":{"type":"string","description":"ISO code"},"description":"Allowlist — empty allows all, else ONLY these"},"rate_limit":{"type":"integer","description":"Max requests per IP per window; 0 = off"},"rate_window":{"type":"integer","description":"Rate window seconds (60–3600, default 60; clamped — the edge counter is a KV key whose TTL is this window, and KV rejects a TTL under 60)"}},"additionalProperties":false}},{"name":"get_lander_source","title":"Get lander source","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /lander-source"],"description":"The GitHub repo the edge router pulls lander HTML from for this workspace (owner, repo, branch). The PAT is never returned — only a connected flag + last-4 fingerprint.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"set_lander_source","title":"Set lander source","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /lander-source"],"description":"Configure the GitHub repo + read-only fine-grained PAT (Contents:read) the router uses to serve landers. Leave token blank to keep the current one. Live at the edge in seconds.","inputSchema":{"type":"object","properties":{"owner":{"type":"string","description":"GitHub owner/org, e.g. \"carsoncraig1\""},"repo":{"type":"string","description":"Repo name, e.g. \"alexiares\""},"branch":{"type":"string","description":"Branch to read (default \"main\")"},"base_dir":{"type":"string","description":"Optional sub-directory to scope landers to, e.g. \"public\" or \"public/fb\"; blank = whole repo"},"asset_base":{"type":"string","description":"Optional origin to rewrite root-absolute lander asset paths (/images, /privacy.html) to at serve time; blank = leave untouched"},"token":{"type":"string","description":"Fine-grained PAT with Contents:read; blank keeps the stored one"}},"additionalProperties":false}},{"name":"validate_lander","title":"Validate a repo lander","category":"settings","scope":"landers:write","readOnly":true,"destructive":false,"covers":["GET /lander-source/validate"],"description":"Fetch a lander file from the configured GitHub repo and check it is ready: it exists and has at least one data-rc-offer CTA (rc.js is injected at serve time, so it need not be in the file). Also reports leftover 3rd-party pixels, and every asset reference the router will NOT rewrite to the Asset base URL (srcset, poster, data-src, a path built in JavaScript, an unquoted src=, or a relative path) — those resolve against the tracking domain and 404 on the live page, and nothing at serve time can repair them. Returns {ok, exists, has_offer, has_pixels, issues[], assets:{unrewritten[], page_links[], rewritten}}.","inputSchema":{"type":"object","properties":{"path":{"type":"string","description":"Repo-relative path (under base_dir), e.g. \"public/fb/reco/pg.html\""}},"required":["path"],"additionalProperties":false}},{"name":"list_lander_repos","title":"List connectable repos","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /lander-source/repos"],"description":"List repositories the connected GitHub account can read, for choosing the lander-source repo. Returns [{full_name, owner, name, default_branch, private}].","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_lander_branches","title":"List repo branches","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /lander-source/branches"],"description":"List branches of a repo the connected GitHub account can read, for choosing the lander-source branch.","inputSchema":{"type":"object","properties":{"owner":{"type":"string","description":"Repo owner/org"},"repo":{"type":"string","description":"Repo name"}},"required":["owner","repo"],"additionalProperties":false}},{"name":"list_lander_files","title":"List repo lander files","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /lander-source/tree"],"description":"List every .html file in the configured lander-source repo/branch (under the configured directory), for choosing a lander to add. Paths are relative to the directory — ready to pass as a lander `source`. Returns {files:[...], base_dir, truncated}.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"get_campaign_source","title":"Get campaign source","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /campaign-source"],"description":"The Airtable base this workspace also describes its campaigns in (base, table, field ids). Picking a campaign when a link is created writes that link id back into the campaign row, so the two systems agree without anyone copying an id. The PAT is never returned — only a connected flag + last-4.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"get_campaign_source_link_field","title":"Get campaign source link field","category":"settings","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /campaign-source/link-field"],"description":"Just the field id (fld...) campaign_source writes this workspace's link ids into, or null if unset. Lower privilege than get_campaign_source (no base/table/PAT) - for an external reader (e.g. rp-edge, SENTRY) that only needs to know which Airtable cell to read, never to configure the connection.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"set_campaign_source","title":"Set campaign source","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /campaign-source"],"description":"Configure the Airtable base + PAT used to look campaigns up and write the link id back. Fields are FIELD IDS (fld...), not names, so an Airtable rename cannot break it. Leave pat blank to keep the stored one.","inputSchema":{"type":"object","properties":{"base_id":{"type":"string","description":"Airtable base id, e.g. \"appadLhvwO93pjRrM\""},"table_id":{"type":"string","description":"Campaigns table id, e.g. \"tblGN3scCJzKxA0PA\""},"name_field":{"type":"string","description":"Field ID of the campaign name column (fld...), used for the picker label"},"source_field":{"type":"string","description":"Field ID of the slug / Source ID column (fld...), matched against a link ref"},"link_field":{"type":"string","description":"Field ID of the RoninConv Link ID column (fld...) — the cell written on pick"},"pat":{"type":"string","description":"Airtable PAT with data read + write on that base; blank keeps the stored one"}},"required":["base_id","table_id","link_field"],"additionalProperties":false}},{"name":"disconnect_campaign_source","title":"Disconnect campaign source","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /campaign-source/disconnect"],"description":"Drop the stored Airtable PAT, leaving the base/table/field configuration in place. Campaign picking and link write-back stop; nothing already written is changed.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_source_campaigns","title":"List Airtable campaigns","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /campaign-source/campaigns"],"description":"Campaigns from the configured Airtable base, for choosing which one a link belongs to. Returns [{record_id, name, source_id, link_id, taken_by}] — taken_by names an EXISTING link that already claims that campaign (a cell naming a deleted link reads as free, not taken).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"attach_link_campaign","title":"Attach a link to a campaign","category":"campaigns","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /links/:id/campaign"],"description":"Write this link id into an Airtable campaign row, tying the two systems together. Used on save by the campaign form, and as the repair path when that write-back failed.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Link id (the public path segment)"},"record_id":{"type":"string","description":"Airtable campaign record id, e.g. \"recXXXXXXXXXXXXXX\""}},"required":["id","record_id"],"additionalProperties":false}},{"name":"list_lander_dirs","title":"List repo lander folders","category":"settings","scope":"links:write","readOnly":true,"destructive":false,"covers":["GET /lander-source/dirs"],"description":"List folders in the configured lander-source repo/branch that contain .html files (each with a recursive count), for choosing the lander Directory by exact path. Returns {dirs:[{path,html}], root_html}.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"lander_drift_status","title":"Lander clean/drift status","category":"settings","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /lander-source/status"],"description":"Live contract status of each repo-sourced lander, recorded by the router on serve: whether it was clean or had trackers/competing CTAs stripped. Keyed by repo path. Powers the Landers drift badge.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"disconnect_lander_source","title":"Disconnect GitHub (Landers)","category":"settings","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /lander-source/disconnect"],"description":"Disconnect the GitHub account from the Landers integration — drops the stored token so the router can no longer pull landers until reconnected.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_meta_connections","title":"List Meta connections","category":"meta","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /meta/connections"],"description":"List Meta (Facebook) API connections — one system-user token each, usually one per Business Manager. Shows health (status, last_error, last_synced_at), token last-4, and discovered account/campaign counts.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"add_meta_connection","title":"Add Meta connection","category":"meta","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /meta/connections"],"description":"Connect a Meta system-user (or user) access token. The token is validated against the Graph API, stored server-side (never returned), and the ad-account / campaign / pixel mesh is discovered immediately. Needed token perms: ads_read.","inputSchema":{"type":"object","properties":{"label":{"type":"string","description":"Human label, e.g. the Business Manager name"},"access_token":{"type":"string","description":"Meta access token (sensitive — stored server-side, never echoed back)"},"token_type":{"type":"string","enum":["system_user","user"],"description":"Default system_user"},"app_id":{"type":"string","description":"Optional Meta app id for reference"}},"required":["label","access_token"],"additionalProperties":false}},{"name":"sync_meta_connection","title":"Sync Meta connection","category":"meta","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /meta/connections/:id/sync"],"description":"Force re-discovery (accounts/campaigns/pixels) + account-wide ad-level hourly spend sync for one connection now, bypassing the schedule. Follows every continuation, persists insights page by page and re-buckets hours to UTC. Returns per-connection results; inspect results[].error for failure. Completed pages remain stored after a later error, but the failed account and connection success timestamps do not advance.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (mc_…)"}},"required":["id"],"additionalProperties":false}},{"name":"backfill_meta_connection","title":"Backfill Meta connection","category":"meta","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /meta/connections/:id/backfill"],"description":"Refresh a connection's identity (system-user + Business Manager names) and pull a wider account-wide spend history — ad-level, hourly, re-bucketed to UTC. Follows all continuations and persists insights page by page. Use to seed cost after linking or recover older names. Returns {accounts, spend_rows, days}; spend_rows counts accepted input ad-hours, including overwrites. A later-page failure returns an error, retaining completed pages without advancing the failed account or connection success timestamps.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (mc_…)"},"days":{"type":"integer","description":"Days of history to pull (1–90, default 30)"}},"required":["id"],"additionalProperties":false}},{"name":"meta_sync_all_now","title":"Sync all Meta now","category":"meta","scope":"links:write","readOnly":false,"destructive":false,"covers":["POST /meta/sync-now"],"description":"Force account-wide spend + catalog sync for every Meta connection in the workspace now. Follows all continuations, persists hourly insights page by page and re-buckets hours to UTC. Inspect results[].error: completed pages remain stored after a later failure, but the failed account and connection success timestamps do not advance.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"get_meta_mesh","title":"Get Meta asset mesh","category":"meta","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /meta/mesh"],"description":"The full Meta asset map in one call: each connection (token/system-user → Business Manager) with its ad accounts, and per account the pixels, campaign count and 30-day spend rollup — plus workspace totals. The overview to start from before drilling into list_meta_ad_accounts / list_meta_campaigns.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_meta_ad_accounts","title":"List Meta ad accounts","category":"meta","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /meta/accounts"],"description":"Ad accounts across all connections: currency, IANA timezone, status, non_usd warning (spend recorded as USD at face value in P1). One row per (account, connection that can see it), so an account reachable through two tokens appears twice: canonical_connection_id names the connection that owns and syncs it, and is_duplicate is true on every other connection’s row.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_meta_campaigns","title":"List Meta campaigns","category":"meta","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /meta/campaigns"],"description":"The discovered Meta campaign catalog for link_meta_campaign (many Meta campaigns may back one RC campaign). linked_link_id/name identify the canonical owning RoninConv campaign, if any. only_unlinked hides taken ones; search filters by name substring. SENTRY-assigned IDs can be absent until discovery; list_campaigns still returns them. Retained campaigns without a connection remain visible with connection_label=\"Disconnected\".","inputSchema":{"type":"object","properties":{"only_unlinked":{"type":"boolean","description":"Only campaigns not yet linked to a RoninConv campaign"},"include_id":{"type":"string","description":"With only_unlinked: still include this Meta campaign id (for edit flows)"},"search":{"type":"string","description":"Case-insensitive name substring filter"}},"additionalProperties":false}},{"name":"get_meta_settings","title":"Get Meta settings","category":"meta","scope":"entities:read","readOnly":true,"destructive":false,"covers":["GET /meta/settings"],"description":"Workspace Meta sync frequency (minutes between spend pulls; cron floor 10).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"update_meta_settings","title":"Update Meta settings","category":"meta","scope":"links:write","readOnly":false,"destructive":false,"covers":["PUT /meta/settings"],"description":"Set the Meta spend sync interval in minutes (clamped 10–60).","inputSchema":{"type":"object","properties":{"sync_interval_min":{"type":"integer","description":"Minutes between syncs (10–60)"}},"required":["sync_interval_min"],"additionalProperties":false}},{"name":"list_workspaces","title":"List workspaces","category":"admin","scope":null,"readOnly":true,"destructive":false,"covers":["GET /workspaces"],"description":"Workspaces visible to this credential (API keys see their own; superadmins see all). Session credentials choose the active workspace per-request via X-Workspace.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_workspace","title":"Create workspace","category":"admin","scope":"superadmin","readOnly":false,"destructive":false,"covers":["POST /workspaces"],"description":"Create a workspace (superadmin session only — a new business = a new workspace). id: lowercase slug, 2–31 chars.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Workspace slug, e.g. \"acme\" (^[a-z0-9][a-z0-9-]{1,30}$)"},"name":{"type":"string","description":"Display name (default: the id)"}},"required":["id"],"additionalProperties":false}},{"name":"list_members","title":"List members","category":"admin","scope":"members:manage","readOnly":true,"destructive":false,"covers":["GET /members"],"description":"List workspace members with roles (owner|editor|viewer).","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"add_member","title":"Add member","category":"admin","scope":"members:manage","readOnly":false,"destructive":false,"covers":["POST /members"],"description":"Invite a user to the workspace (or update their role if already invited). Creates the user if the email is new — the response then carries a one-time temp_password to hand them out-of-band.","inputSchema":{"type":"object","properties":{"email":{"type":"string","description":"User email"},"role":{"type":"string","enum":["owner","editor","viewer"],"description":"Role (default viewer)"},"password":{"type":"string","description":"Optional initial password for a brand-new user (default: random temp)"}},"required":["email"],"additionalProperties":false}},{"name":"update_member_role","title":"Update member role","category":"admin","scope":"members:manage","readOnly":false,"destructive":false,"covers":["PATCH /members/:userId"],"description":"Change a member's role in this workspace.","inputSchema":{"type":"object","properties":{"user_id":{"type":"string","description":"User id (u_…) from list_members"},"role":{"type":"string","enum":["owner","editor","viewer"],"description":"New role"}},"required":["user_id","role"],"additionalProperties":false}},{"name":"remove_member","title":"Remove member","category":"admin","scope":"members:manage","readOnly":false,"destructive":true,"covers":["DELETE /members/:userId"],"description":"Remove a member from this workspace (their global user account survives; other workspace memberships are untouched).","inputSchema":{"type":"object","properties":{"user_id":{"type":"string","description":"User id (u_…) from list_members"}},"required":["user_id"],"additionalProperties":false}},{"name":"reset_member_password","title":"Reset member password","category":"admin","scope":"members:manage","readOnly":false,"destructive":true,"covers":["POST /members/:userId/reset-password"],"description":"Admin password reset: mints a one-time temp password (returned once), forces a change at next login, kills the user's sessions and disables their 2FA. Use deliberately — it locks the person out until they use the temp password.","inputSchema":{"type":"object","properties":{"user_id":{"type":"string","description":"User id (u_…) from list_members"}},"required":["user_id"],"additionalProperties":false}},{"name":"list_api_keys","title":"List API keys","category":"admin","scope":"keys:manage","readOnly":true,"destructive":false,"covers":["GET /keys"],"description":"List workspace API keys (id, label, scopes, revoked_at). Secrets are never shown after minting.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"create_api_key","title":"Create API key","category":"admin","scope":"keys:manage","readOnly":false,"destructive":false,"covers":["POST /keys"],"description":"Mint a workspace-pinned API key. The full secret (\"rk_….…\") is returned ONCE — store it immediately. Scope \"full\" = every ordinary permission, but NOT the explicit-opt-in ones (\"meta_spend:write\", \"ops:read\"), which a key only gets by naming them outright; or pass a narrow scope list (e.g. [\"reports:read\",\"entities:read\"] for a read-only agent).","inputSchema":{"type":"object","properties":{"label":{"type":"string","description":"Key label, e.g. \"mcp-agent\" (default \"unlabeled\")"},"scopes":{"type":"array","items":{"type":"string","enum":["full","reports:read","entities:read","links:write","landers:write","offers:write","domains:write","webhooks:manage","conversion_events:write","keys:manage","members:manage"]},"description":"Scopes (default [\"full\"])"}},"additionalProperties":false}},{"name":"revoke_api_key","title":"Revoke API key","category":"admin","scope":"keys:manage","readOnly":false,"destructive":true,"covers":["DELETE /keys/:id"],"description":"Revoke an API key immediately and permanently. If it is the key this MCP session authenticates with, subsequent calls will fail.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Key id (rk_…) from list_api_keys"}},"required":["id"],"additionalProperties":false}},{"name":"list_security_events","title":"List security events","category":"admin","scope":"members:manage","readOnly":true,"destructive":false,"covers":["GET /security/events"],"description":"Security audit trail (last 200): logins, failures, lockouts, password/2FA changes, admin resets, OAuth grants — with IP and user-agent.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"list_oauth_connections","title":"List OAuth connections","category":"admin","scope":"keys:manage","readOnly":true,"destructive":false,"covers":["GET /oauth/connections"],"description":"OAuth connections into this workspace (e.g. claude.ai connectors): connection id, client name, authorizing user, access level (full|read), created/last-used/expiry, revoked state. These are the workspace-pinned tokens minted through the /oauth consent flow — the OAuth counterpart of list_api_keys.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"revoke_oauth_connection","title":"Revoke OAuth connection","category":"admin","scope":"keys:manage","readOnly":false,"destructive":true,"covers":["DELETE /oauth/connections/:id"],"description":"Revoke one OAuth connection immediately — its access AND refresh tokens die, so the connected client (e.g. a claude.ai connector) must re-authorize through consent.","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (oc_…) from list_oauth_connections"}},"required":["id"],"additionalProperties":false}}],"exempt_routes":{"GET /health":"infra liveness probe, no data","POST /ingest/meta-spend":"server-to-server push (SENTRY's fast cost feed) — workspace-pinned meta_spend:write key; final valid ad/UTC-instant snapshot wins per batch, upserted counts distinct valid facts; not an AI-agent action","GET /.well-known/oauth-authorization-server":"OAuth discovery metadata (RFC 8414) — MCP plumbing","GET /.well-known/oauth-authorization-server/mcp":"OAuth discovery metadata, path-scoped variant","GET /.well-known/oauth-protected-resource":"OAuth resource metadata (RFC 9728) — MCP plumbing","GET /.well-known/oauth-protected-resource/mcp":"OAuth resource metadata, path-scoped variant","POST /oauth/register":"OAuth dynamic client registration (RFC 7591) — client plumbing","GET /oauth/authorize":"human consent page (login + workspace picker) — interactive by design","POST /oauth/authorize":"human consent form submission — interactive by design","POST /oauth/token":"OAuth token exchange — client plumbing, not an agent action","POST /auth/login":"human session bootstrap — agents use API keys or OAuth","POST /auth/logout":"session-only","GET /auth/status":"session-only (2FA / must-change-pw UI state)","POST /auth/password":"human credential change, deliberately interactive","POST /auth/2fa/setup":"human 2FA enrollment, deliberately interactive","POST /auth/2fa/enable":"human 2FA enrollment, deliberately interactive","POST /auth/2fa/disable":"human 2FA removal, deliberately interactive","GET /auth/sessions":"session-only (browser device list)","POST /auth/sessions/revoke-all":"session-only (browser device management)","POST /lander-source/oauth/start":"GitHub OAuth initiation — returns a browser authorize URL, interactive by design","GET /lander-source/oauth/callback":"GitHub OAuth redirect target — browser callback, not an agent action","POST /cloudflare/oauth/start":"Cloudflare OAuth initiation — returns a browser authorize URL, interactive by design","GET /cloudflare/oauth/callback":"Cloudflare OAuth redirect target — browser callback, not an agent action","POST /google/oauth/start":"Google OAuth initiation — returns a browser authorize URL, interactive by design","GET /google/oauth/callback":"Google OAuth redirect target — browser callback, not an agent action"}}