# Stopping a lead

Stop one lead's automatic sequence. New attempts are blocked, but a send that already started can still complete.

> **Note.** Write access is enabled for your organization on request.

## Request

`POST /v1/leads/{id}/stop` requires `leads:stop` and scope access to the lead's workspace and campaign. `id` identifies one participation: stopping it does not stop the same person in other campaigns or add them to the general stop list.

Send the required `Idempotency-Key` (1 to 255 printable ASCII characters), `Content-Type: application/json` and an empty object `{}`. Extra fields are rejected. Persist a unique key for this operation before requesting; do not reuse an enrollment operation key for a stop.

```bash
curl --retry 5 --retry-max-time 300 \
  -X POST https://api.melioralab.io/v1/leads/5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31/stop \
  -H "Authorization: Bearer $MELIORA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $MELIORA_IDEMPOTENCY_KEY" \
  --data-raw '{}'
```
```python
import os
import random
import time
import requests

headers = {
    "Authorization": f"Bearer {os.environ['MELIORA_API_KEY']}",
    "Content-Type": "application/json",
    "Idempotency-Key": os.environ["MELIORA_IDEMPOTENCY_KEY"],
}
for attempt in range(6):
    response = requests.post(
        "https://api.melioralab.io/v1/leads/5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31/stop",
        headers=headers, data=b"{}", timeout=15,
    )
    if response.status_code in (429, 503) and attempt < 5:
        time.sleep(float(response.headers.get("Retry-After", "5")) + random.random())
        continue
    response.raise_for_status()
    command = response.json()
    print(command["id"], command["result"])
    break
```
```js
const headers = {
  Authorization: `Bearer ${process.env.MELIORA_API_KEY}`,
  'Content-Type': 'application/json',
  'Idempotency-Key': process.env.MELIORA_IDEMPOTENCY_KEY,
};
for (let attempt = 0; attempt < 6; attempt++) {
  const response = await fetch('https://api.melioralab.io/v1/leads/5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31/stop', {
    method: 'POST', headers, body: '{}', signal: AbortSignal.timeout(15_000),
  });
  if ([429, 503].includes(response.status) && attempt < 5) {
    const delay = Number(response.headers.get('Retry-After') ?? 5) + Math.random();
    await response.body?.cancel();
    await new Promise((resolve) => setTimeout(resolve, delay * 1000));
    continue;
  }
  if (!response.ok) throw new Error(`Meliora API ${response.status}: ${await response.text()}`);
  const command = await response.json();
  console.log(command.id, command.result);
  break;
}
```

## Interpreting the response

HTTP 202 returns a [command](https://docs.melioralab.io/en/reference/commands/) with `kind=stop`, the target `lead_id`, empty `items` and these result fields:

- `result.automation_state=stopped`: new automatic attempts are blocked and there is no in-flight send.
- `result.automation_state=stopping`: the block is set, but a send already started.
- `result.send_in_flight=true`: that send can still complete. This is not delivery confirmation.
- `result.unknown_outcome_message_ids`: opaque identifiers of sends with unknown outcomes. Do not parse their format.
- `cleanup_pending=true`: stopped work still needs cleanup. This is a command field, outside `result`.

The command may already be `done` or `done_with_unknown`, or remain `accepted` or `running`. Poll [`GET /v1/commands/{id}`](https://docs.melioralab.io/en/commands/) until a terminal state and completed cleanup. `stopped` in the result alone does not replace that check.

> **Warning.** A stop does not recall a sent message. `done_with_unknown` means the stop is complete and no new attempts will start, but a past outcome remains unknown. Do not send the message again because its outcome is unknown.

When [reading the lead](https://docs.melioralab.io/en/reference/leads/), inspect `automation_stopped`, `automation_stopped_at`, `automation_stop_source` and `automation`: `state` (`running`, `stopping`, `stopped`, `unknown`), `stopped_at`, `source`, `send_in_flight`, `unknown_outcome_message_ids`. Read responses allow `null` for `automation_stopped`, `automation.send_in_flight` and `automation.unknown_outcome_message_ids` when data is not established. `null` does not mean `false` or an empty array. The participation status, such as `replied`, does not have to change on stop.

`automation.state=running` means no lead-level stop is established; it does not grant permission to send immediately. Campaign pauses and holds still apply. Unlike reads, the stop command result has a definite `send_in_flight` (`true` or `false`) and an array of `unknown_outcome_message_ids`.

## Event and retries

`lead.stopped` appears in the [event feed](https://docs.melioralab.io/en/events-polling/) once in-flight work and cleanup settle, not immediately at `stopping`. `data.stop` contains `automation_stopped`, `stopped_at` and `source`. The schema allows `true`, `false` or `null` for `automation_stopped`, a time or `null` for `stopped_at`, and a string or `null` for `source`. An established API stop is expected to have `true` and `api`, but those are not schema constants. It confirms stopped automation, not certainty about every past send.

Retrying the same key, lead and body returns the same command in its current state. After 429/503 wait for `Retry-After`; after a lost connection preserve the same key. Stopping again with a new key is also safe: it does not resume the sequence, preserves the original stop time, and creates no second transition or second `lead.stopped` event. A different lead or operation with an already used key gives 422 `idempotency_key_reused`.

## Budget and errors

Stop weight is 1 with a separate budget: up to 2 concurrent requests, 60 per minute per key and 120 per minute per organization. It does not consume the organization's normal minute budget or normal concurrency slots. Limits do not guarantee an immediate response; retry 429/503 after the required delay.

403 `scope_required` means a missing permission; 403 `api_write_disabled` means writes are not enabled yet. An invalid body gives 422 `invalid_request`, an invalid header 400 `invalid_parameter`, and an inaccessible or missing lead 404 `not_found`. After 503 `rate_limiter_unavailable`, retry the original request with the same key. See [errors](https://docs.melioralab.io/en/errors/) and [limits](https://docs.melioralab.io/en/rate-limits/).
