> ## Documentation Index
> Fetch the complete documentation index at: https://developers.everflow.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Write Tools

> Let an AI agent change offer and account status and adjust conversions, on API keys you have opted in, with an optional dry-run preview.

> [!WARNING]
>
> **Write tools are opt-in, per API key.** They appear only when the key's **MCP access** is set to **Full access** (see [MCP access](https://developers.everflow.io/ai-automation/mcp/overview.md#mcp-access)) **and** the key holds the module permission the tool needs. Every key starts at **Read Only**, which hides all four write tools. If you do not see them, check both settings on the key under **Control Center → Security**.

Alongside the read tools, a focused set of **write tools** lets an AI agent *act* on your network: pause an underperforming offer, activate or suspend a partner or advertiser, and approve, reject, or correct conversions. These are the operational moves that make up most day-to-day account management, available to an agent in the same conversation where it surfaced the problem. The set is intentionally scoped to status and conversion changes rather than record creation or arbitrary field edits; anything beyond it goes through the [Network API](https://developers.everflow.io/api-reference/network-overview.md).

| Tool | Changes | Required permission (with MCP access at Full access) |
| - | - | - |
| [manage\_offer](#manage_offer) | Offer status (active ↔ paused) | **Offer → Manage** (Full access) |
| [manage\_affiliate](#manage_affiliate) | Partner account status | **Partner → Manage** (Full access) |
| [manage\_advertiser](#manage_advertiser) | Advertiser account status | **Advertiser → Manage** (Full access) |
| [manage\_conversion](#manage_conversion) | Conversion status, payout/revenue, sale amount, notes | **Reporting → Conversion** |

**Reporting → Conversion** has no read-only setting, so on a key with MCP access at Full access, conversion access means the agent can change conversions. Give such a key MCP write access only if that is what you intend.

> [!NOTE]
>
> **After an Everflow MCP release, or after changing a key's MCP access, reconnect your client.** Clients cache the tool list at connect time and validate arguments against it, so a long-lived session can still advertise retired actions, miss tools the key has just been given, and reject new arguments locally before the call reaches Everflow. See [Staying current after a release](https://developers.everflow.io/ai-automation/mcp/quickstart.md#staying-current-after-a-release).

## The `action` parameter

Every write tool takes a required **`action`** naming the operation. `manage_offer`, `manage_affiliate` and `manage_advertiser` each accept one action, `set_status`; `manage_conversion` accepts four. The parameter is a discriminator, so these tools can gain further operations later without changing the meaning of any call you write today.

**Arguments are validated per action, not per tool.** An argument that is valid for one action is rejected for another: sending `payout` alongside `action="set_status"` returns `INVALID_ARGUMENT` naming the argument, rather than ignoring it. Combined with the unknown-argument rejection below, a malformed write fails loudly instead of applying a partial change.

These tools are annotated **destructive**, which is a *hint* to your MCP client. Some clients prompt the user before invoking one; others do not, and an auto-approving or headless agent will not prompt at all. **Treat the prompt as a convenience, not a control:** the server implements no approval handshake. The controls you can rely on are the key's MCP access, its module permissions, the narrow set of actions, and the server-side audit trail.

## Previewing a change with `dry_run`

Every write tool accepts an optional `dry_run` argument.

* **`dry_run=true`**: runs every validation and existence check, then **previews** the change without persisting anything. The response carries `preview: true` and the **current → new** values so you can show the exact effect before applying.
* **`dry_run` omitted or `false`**: **applies** the change. For most conversion actions, "applies" means "submits"; see [Propagation](#propagation-synchronous-vs-asynchronous).

`dry_run` is a preview, **not** an approval gate: an automated caller can dry-run and immediately re-call. Use it to show the resolved change to a person before executing, and do that confirmation yourself rather than assuming your client will prompt.

> [!NOTE]
>
> **`dry_run` must be a boolean.** `true` and `false`, a number (`0` is false, any other number is true) and the strings `"true"`, `"True"`, `"TRUE"`, `"t"`, `"T"`, `"1"` and their false counterparts are accepted. Anything else (`"yes"`, `null`, an array, an object) is **rejected** with `INVALID_ARGUMENT`, so the call fails rather than falling through to a real write.

```json Dry-run preview (manage_offer, dry_run=true) theme={null}
{
  "preview": true,
  "network_offer_id": 2,
  "name": "ExpressVPN Premium - CPA $48",
  "current_offer_status": "active",
  "offer_status": "paused"
}
```

```json Execute (manage_offer, dry_run omitted) theme={null}
{
  "network_offer_id": 2,
  "offer_status": "paused"
}
```

## Propagation: synchronous vs asynchronous

Propagation is **not uniform**, and for conversions this changes what a successful response means.

* **Offer and account status changes** (`manage_offer`, `manage_affiliate`, `manage_advertiser`) are **synchronous**: a successful response means the change is applied.
* **Conversion `set_notes`** is **synchronous**.
* **Conversion `set_status`, `set_payout_revenue` and `set_sale_amount`** are **asynchronous**. They are **submitted** to the same processing pipeline the Network API's conversion endpoints use. **A successful response means the request was accepted, not that the change is applied.** The change is processed a few seconds later, and the pipeline runs further validation that can still reject or adjust it after the call returned success.

> [!WARNING]
>
> **An asynchronous response means submitted, not applied: verify before reporting success.** An asynchronous rejection is never pushed back to the caller. To confirm, wait a few seconds (processing typically completes in under 10), then re-read the conversion with `get_entity(type="conversion")`. If it shows the requested value, it applied. If it still shows the old value, it has not applied yet; "still pending" and "rejected during processing" look the same from outside. **If it is still unchanged after about 30 seconds, re-submit the same call.** A repeated `set_status` to the same status, or a repeated `set_payout_revenue` with the same values, is a no-op in the pipeline, so a retry cannot apply a change twice. This check works whenever the requested value differs from the current one; if they were already identical, the call was a no-op and there is nothing to confirm.

Every `manage_conversion` response carries two fields to make this explicit:

| Field | Value | Meaning |
| - | - | - |
| `propagation` | `"asynchronous"` / `"synchronous"` | Whether the change was submitted to the pipeline or applied in-line |
| `next_step` | string | Guidance restating the above for this specific call |

```json Async submit (manage_conversion, set_status, dry_run omitted) theme={null}
{
  "action": "set_status",
  "propagation": "asynchronous",
  "next_step": "SUBMITTED for asynchronous processing: accepted, NOT applied yet and NOT guaranteed. ...",
  "conversion_ids": ["mcpt...0002"],
  "conversion_status": "approved",
  "summary": {
    "entity_count": 1,
    "entity_noun": "conversion",
    "unchanged_count": 0,
    "status_transition": "Pending → Approved",
    "totals": [
      { "label": "Payout affected", "value": "10.00 USD" },
      { "label": "Revenue affected", "value": "25.00 USD" }
    ]
  }
}
```

In a `set_status` summary, conversions already at the requested status are counted in `unchanged_count` and left out of `entity_count` and the totals. When every conversion is already at the requested status, `status_transition` reads `"No change"`. Amount changes are compared to six decimal places, so a sub-cent difference (`1.494` → `1.49`) shows as a change.

***

## manage\_offer

Changes an offer's **status**. It does not edit caps, visibility, payout/revenue, targeting, URLs, names, or any other field, and it cannot create or delete offers. Editing an offer's payout/revenue is not available through MCP: use the portal or the [Network API](https://developers.everflow.io/api-reference/network-overview.md).

**Parameters:**

| Parameter | Type | Required | Description |
| - | - | - | - |
| `action` | string | Yes | `set_status` |
| `offer_id` | number | Yes | The offer to update |
| `offer_status` | string | Yes | `active` or `paused` only. `pending` and `deleted` are rejected |
| `dry_run` | boolean | No | Preview without persisting (default `false`, which executes). Recommended before applying; see [Previewing a change with `dry_run`](#previewing-a-change-with-dry_run) |

**Action constraints:**

| Action | Notes |
| - | - |
| `set_status` | `active` or `paused` only. **Pausing an offer stops new traffic:** clicks are refused (or sent to fail traffic if your network has that setting on), and new conversions are held as pending. A **pending** offer (not live yet) can be set to active or paused. A **deleted** offer cannot be changed here: the call is rejected; restore it in the portal first. Setting an offer to the status it already has succeeds and changes nothing, not even an audit entry. |

> [!NOTE]
>
> `dry_run=true` previews the resolved change; it is **not** an approval gate, and neither is your client's prompt, since some clients do not prompt at all. Confirm the resolved change with the user before re-calling without `dry_run`.

***

## manage\_affiliate

Changes a partner's `account_status`. It does not edit other account fields and cannot create or delete accounts.

**Parameters:**

| Parameter | Type | Required | Description |
| - | - | - | - |
| `action` | string | Yes | `set_status` |
| `affiliate_id` | number | Yes | The partner to update |
| `account_status` | string | Yes | `active`, `inactive`, or `suspended`. `pending` is **not** allowed |
| `dry_run` | boolean | No | Preview without persisting (default `false`, which executes) |

> [!NOTE]
>
> `inactive` is a soft turn-off (offboarding, dormant); `suspended` is a stronger, punitive state (violation, fraud). `pending` is an unapproved application handled by a separate approval flow: it cannot be set here, and a partner that is **currently** pending cannot be changed here either. Approve or reject it in the Everflow UI first.

***

## manage\_advertiser

Changes an advertiser's `account_status`. Same shape as `manage_affiliate`.

**Parameters:**

| Parameter | Type | Required | Description |
| - | - | - | - |
| `action` | string | Yes | `set_status` |
| `advertiser_id` | number | Yes | The advertiser to update |
| `account_status` | string | Yes | `active`, `inactive`, or `suspended`. `pending` is **not** allowed |
| `dry_run` | boolean | No | Preview without persisting (default `false`, which executes) |

An advertiser that is **currently** pending cannot be changed here: approve or reject it in the Everflow UI first.

***

## manage\_conversion

Updates existing conversions: approve or reject, adjust payout and revenue, set the sale amount, or edit notes. **Most actions are asynchronous**; see [Propagation](#propagation-synchronous-vs-asynchronous).

**Parameters:**

| Parameter | Type | Required | Description |
| - | - | - | - |
| `action` | string | Yes | `set_status`, `set_payout_revenue`, `set_sale_amount`, or `set_notes` |
| `conversion_ids` | array of strings | Yes | Conversion ID(s). `set_status` accepts up to **10** per call; the other actions take **exactly one**. Repeated IDs are counted once |
| `conversion_status` | string | For `set_status` | `approved` or `rejected` (case-insensitive) |
| `payout` | number | For `set_payout_revenue` | New payout (**both** payout and revenue required) |
| `revenue` | number | For `set_payout_revenue` | New revenue (**both** required) |
| `sale_amount` | number | For `set_sale_amount` | New sale amount |
| `notes` | string | For `set_notes` | New notes (max **500** characters, BMP only). An empty string clears the notes |
| `dry_run` | boolean | No | Preview without persisting (default `false`, which executes) |

**Action constraints:**

| Action | Cardinality | Propagation | Notes |
| - | - | - | - |
| `set_status` | up to 10 IDs | asynchronous | `approved` or `rejected` only. Batch larger sets across calls. |
| `set_payout_revenue` | exactly 1 | asynchronous | **Both** `payout` and `revenue` required. Only allowed on **approved** conversions (scrubbed conversions are exempt): a conversion in any other status is rejected immediately, and `dry_run` applies the same check. |
| `set_sale_amount` | exactly 1 | asynchronous | |
| `set_notes` | exactly 1 | **synchronous** | Applied immediately. A value over 500 characters is rejected, not truncated. Characters outside the Basic Multilingual Plane (most emoji) are rejected, because the column cannot store them; symbols such as ✅ are fine. A number is stored as its text (`12345` becomes `"12345"`); a boolean, `null`, an array or an object is rejected. |

**Amounts.** `payout`, `revenue` and `sale_amount` must be numbers below 10,000,000,000,000 in magnitude; a non-numeric value is rejected rather than written as `0`. Negative amounts are accepted, except on networks configured to refuse them, where a negative value returns `INVALID_ARGUMENT` (`payout and revenue cannot be negative on this network`, or the `sale_amount` equivalent). Both checks also run on `dry_run`.

> [!NOTE]
>
> Do not change a conversion's payout or revenue right after changing its status. While the status change is still queued, the approved-only check sees the old status. Re-read the conversion with `get_entity` and confirm its status first.

> [!NOTE]
>
> Passing more than 10 IDs returns `INVALID_ARGUMENT: Too many conversion_ids: N (max 10 per call)`. The limit is deliberately small: these writes are asynchronous with no confirmation channel, so it bounds how much a single call can change before it can be verified. A conversion ID that doesn't exist, or that your key isn't allowed to manage, returns `NOT_FOUND`. Validation is all-or-nothing: if any ID in the batch is missing or outside your scope, the whole call is rejected and nothing is submitted. Once validated, each conversion is submitted separately, so verify each one as described under [Propagation](#propagation-synchronous-vs-asynchronous). You can only act on conversions for partners you are allowed to manage.

***

## Errors

Write tools use the same structured error envelope as the read tools; see [Error responses](https://developers.everflow.io/ai-automation/mcp/tools.md#error-responses). The codes you'll see most often here:

| Code | Common trigger |
| - | - |
| `INVALID_ARGUMENT` | A missing or unsupported `action`, an argument not valid for the chosen action, a disallowed status (`pending` or `deleted` on an offer; `pending` on an account), a change to a deleted offer or to a currently pending partner or advertiser, a missing required field (`revenue` without `payout`), more than 10 conversion IDs, an amount that is not a number or is too large, a negative amount on a network that refuses them, `set_payout_revenue` on a conversion that is not approved, notes over 500 characters or containing non-BMP characters, a non-boolean `dry_run`, or an unrecognized argument name |
| `PERMISSION_DENIED` | The key's MCP access is Read Only (the message says so), or the key lacks the module's write permission |
| `NOT_FOUND` | A referenced offer, partner, advertiser or conversion ID does not exist, or a partner or conversion is outside your key's partner scope |
| `INTERNAL` | The change could not be applied because of a server-side error. Re-read the record before retrying |

> [!NOTE]
>
> **Unknown arguments are rejected, not ignored.** On a write tool, an unrecognized parameter name (`status` instead of `conversion_status`) returns `INVALID_ARGUMENT` naming it and listing the valid parameters, with a did-you-mean suggestion when the name is a close misspelling (`conversion_staus`), so a typo can never take a default and corrupt the change. The allow-list is per **action**, so an argument belonging to a different action of the same tool is rejected the same way.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.