# Campaign Steps

> One campaign's results per sequence step and variant.

Requires an API key with the `read` scope.

Counts follow the Reports page: a bounced email counts as sent, replies are distinct leads, and replies from bounce, warmup and unrelated mail are left out. Days are bucketed in the timezone of the user who created the API key.

These are the Reports numbers narrowed to one campaign. The [campaign totals](/api-reference/analytics/campaign-stats) endpoint counts differently (from the campaign's lifetime counters), so the two can disagree.

## Path Parameters

<ParamField path="campaign_id" type="string" required>The campaign id.</ParamField>

## Query Parameters

<ParamField query="start" type="string">First day, `YYYY-MM-DD`. Defaults to 30 days before `end`.</ParamField>
<ParamField query="end" type="string">Last day, `YYYY-MM-DD`. Defaults to today in the key owner's timezone. A range is at most 365 days.</ParamField>

## Response

<ResponseField name="data" type="object">
  <Expandable title="data">
    <ResponseField name="range" type="object">`start`, `end` and the `timezone` used to bucket days.</ResponseField>
    <ResponseField name="steps" type="array">One row per step and variant with `step`, `stepName`, `variant` and the counters.</ResponseField>
  </Expandable>
</ResponseField>

<RequestExample>
```bash cURL
curl "https://api.foxreach.io/api/v1/analytics/campaigns/cmp_abc123/steps" \
  -H "X-API-Key: otr_your_key"
```

```python Python SDK
from foxreach import FoxReach

client = FoxReach(api_key="otr_your_key")
rows = client.analytics.campaign_steps("cmp_abc123")
```

```typescript TypeScript SDK
import { FoxReach } from "foxreach";

const client = new FoxReach({ apiKey: "otr_your_key" });
const rows = await client.analytics.campaignSteps("cmp_abc123");
```
</RequestExample>

<ResponseExample>
```json
{
  "data": {
    "range": {"start": "2026-10-06", "end": "2026-11-04", "timezone": "UTC"},
    "steps": []
  }
}
```
</ResponseExample>

## Errors

| Status | Description |
|--------|-------------|
| `401` | Missing, invalid, revoked or expired API key |
| `403` | API key does not have the required scope: `read` |
| `400` | `start` is after `end`, or the range is longer than 365 days |
| `404` | `Campaign not found`: the campaign does not exist in this workspace |

## Rate Limit

- **300 requests per minute, shared by every `read` call in your workspace.** Fixed 60-second window.
- Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (unix epoch seconds) and `X-RateLimit-Bucket` (`read`).
- A `429` response includes a `Retry-After` header (seconds until the bucket resets).

For client-side patterns, see [Rate Limiting](/rate-limiting).
