# Autopilot

Autopilot watches your connected store and its content, plans campaigns, and has AI write them as ordinary drafts — an email, a text, or both. Every proposal waits for a person's yes until a kind of campaign earns sending on its own, and Autopilot is off until an admin turns it on.

## Access

Every call takes a Full access API key; a Sending access key is `401 restricted_api_key`. An OAuth token needs the scopes below.

| To | Needs |
| --- | --- |
| Read the settings and proposals | `autopilot:read`. |
| Skip a proposal | `autopilot:write`. |
| Approve a proposal | `autopilot:write` and `broadcasts:write`, because approving schedules campaigns. |
| Read the playbook | `autopilot:read`. |
| Read the campaign kinds | `autopilot:read`. |
| Put a kind on Auto, or take it off | `autopilot:write`, and an admin, as for the settings: a kind on Auto schedules campaigns with nobody's yes. |
| Cancel a scheduled proposal | `autopilot:write` and `broadcasts:write`, because it cancels campaigns. |
| Use, dismiss, restore or reset the playbook | `autopilot:write`, and an admin, as for the settings: it changes what Autopilot proposes and how it writes. |
| Change the settings | `autopilot:write`, and an admin: a team admin, or a Full access key an admin created. Anyone else is `403 invalid_permission`. An OAuth token cannot change them. |

Nothing on these routes calls an AI model. They read and decide what is already there.

## Lifecycle

`drafting` → `proposed` → `scheduled` → `sent`, or `skipped`, `expired` or `failed`. Only a `proposed` proposal can be approved or skipped.

| Status | Means |
| --- | --- |
| `drafting` | Autopilot is writing the campaigns. Nothing to decide yet. |
| `proposed` | Waiting for a person. The only status you can approve or skip. |
| `approved` | The moment between a yes and the schedule. An approval, a person's or Auto's, passes straight through it. |
| `scheduled` | Approved, by a person or, with `auto: true`, by itself. Its campaigns are scheduled at the proposal's send time. |
| `sent` | Its campaigns went out. |
| `skipped` | Someone said no, or every scheduled campaign was canceled. |
| `expired` | Nobody decided before its send time, or before its `expires_at`. Its drafts are deleted. |
| `failed` | Its campaigns could not be sent. |

The brief's facts — product names, prices, stock, links, pictures — come from your store's catalogue, and the audience size, `recipient_estimate`, is counted by Rasket. None of them comes from the AI.

## The drafts are broadcasts

- `email_broadcast_id` and `sms_broadcast_id` are ordinary draft campaigns. Change one with `PATCH /broadcasts/{id}` before you approve, and the edit is what goes out.
- Approving runs each draft through the gate and content checks of `POST /broadcasts/{id}/send`, together. If either fails, nothing is scheduled: the answer is `422` with the failing checks in `details.checks`, and `acknowledge_checks: true` schedules anyway.
- Sending a proposal's draft yourself with `POST /broadcasts/{id}/send` counts as approving the proposal.
- Canceling every scheduled campaign of an approved proposal ends it as `skipped`, with `skip_reason` `canceled`.
- A proposal nobody decides before its send time, or its `expires_at`, becomes `expired` and its drafts are deleted. Rasket checks every five minutes.

## Skipping

A skip deletes the drafts and keeps your reason and note, which Autopilot learns from.

| Reason | Means |
| --- | --- |
| `not_relevant` | Not something your customers care about. |
| `wrong_timing` | Right idea, wrong day. |
| `wrong_audience` | The wrong people. |
| `off_brand` | It does not sound like you. |
| `wrong_facts` | Something in it is wrong. |
| `too_frequent` | Too many campaigns. |
| `other` | Anything else. Say what in `note`. |

## What it learns

Every week Autopilot looks at what its sent campaigns earned and how they were clicked, at what was changed in its drafts before they were approved, and at the reasons for skips. It proposes a new version of the store's `playbook`: how much each kind of campaign is worth to this store, the hour campaigns do best at, and the style to write in. A version changes nothing until an admin uses it.

- Every number in it is worked out from the store's own results. Only the style notes and phrases are written by AI, from the edits.
- A campaign that uses one of the live version's `avoid_phrases` fails its checks, as a wrong price does.
- Each proposal records the version it was planned and written with.

## Earned auto-send

A kind of campaign can earn sending without asking. `GET /autopilot/kinds` shows each kind's bars over the last 90 days: at least 10 decided, 80% approved, 70% of approvals with few edits, unsubscribes at most 0.5%, spam complaints at most 0.08%, and sales — or clicks, for a store with no sales yet — at least half its usual per recipient. A kind that clears every bar is `ready`.

- Nothing goes on Auto by itself. With `approval_mode` `earned`, an admin puts a `ready` kind on Auto with `PATCH /autopilot/kinds/{kind}`.
- A proposal of that kind whose checks and quality score passed then schedules itself, as long as its send is at least four hours away. It is `scheduled` with `auto: true`, and the people Autopilot tells get an email with a Cancel link.
- `POST /autopilot/proposals/{id}/cancel` cancels it until 30 minutes before it sends. After that, cancel the campaign itself.
- Every hour Autopilot checks each kind on Auto again. If unsubscribes, complaints or sales slip, one campaign draws a complaint spike, or two of its campaigns are canceled, the kind goes back to asking and the owners are told why. After a complaint spike, its campaigns that have not started are canceled too.

## Events

Subscribe a webhook to these to hear about proposals as they change. Their `data` is the proposal without its `brief`; the [events page](https://www.rasket.com/docs/events) has a payload for each.

| Type | Means |
| --- | --- |
| `autopilot.proposal.created` | A proposal is ready for a decision. |
| `autopilot.proposal.approved` | It was approved and its campaigns are scheduled. |
| `autopilot.proposal.skipped` | Someone skipped it, or its campaigns were canceled. |
| `autopilot.proposal.expired` | Nobody decided in time. |
| `autopilot.proposal.sent` | Its campaigns went out. |

> An agent can review proposals over [MCP](https://www.rasket.com/docs/mcp): `get_autopilot_proposal` shows who a proposal reaches, when, and its drafts, and `approve_autopilot_proposal` runs only with the confirmation token it returned.

## Endpoints

### `GET /autopilot/settings`

What Autopilot may do, for this project.

Retrieve Autopilot's settings:

```sh
curl -X GET "https://api.rasket.com/autopilot/settings" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/settings", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/autopilot/settings",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "autopilot_settings",
  "enabled": true,
  "store_id": "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
  "channels": ["email", "sms"],
  "kinds_enabled": ["new_arrivals", "back_in_stock", "sale", "blog_post"],
  "max_emails_per_week": 2,
  "max_sms_per_week": 1,
  "min_hours_between_per_contact": 48,
  "quiet_hours_start": "21:00",
  "quiet_hours_end": "08:00",
  "time_zone": "America/New_York",
  "default_segment_id": "0199d2c1-5a32-7b44-9c55-7d6e8f9a0b12",
  "topic_id": null,
  "approval_mode": "ask",
  "monthly_credit_cap": 200,
  "notify_member_ids": ["0199c4e2-1f7a-7b30-8e52-3d9c6a1b2f48"],
  "voice_notes": "Warm and brief. Lead with the product, never with the discount.",
  "updated_at": "2026-09-08T16:30:12.004Z"
}
```

- Before an admin saves anything this is the defaults: off, email only, every kind of campaign, and `updated_at: null`.

### `PATCH /autopilot/settings`

Turn Autopilot on or off and set its limits. Only the fields present are touched.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `enabled` | boolean | Whether Autopilot plans campaigns. Defaults to `false`. |
| `store_id` | string \| null | The store it plans for. A store that is not this project's, or is disconnected, is `422`. |
| `channels` | string[] | `email`, `sms` or both: what it may write. At least one. Defaults to `email`. |
| `kinds_enabled` | string[] | The kinds of campaign it may propose: `new_arrivals`, `price_drop`, `back_in_stock`, `low_stock`, `bestsellers`, `seasonal`, `sale` or `blog_post`. At least one. Defaults to all of them. |
| `max_emails_per_week` | integer | 0–14. Defaults to 2. |
| `max_sms_per_week` | integer | 0–7. Defaults to 1. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `min_hours_between_per_contact` | integer | The fewest hours between two Autopilot campaigns to one person, 0–336. Defaults to 48. |
| `quiet_hours_start` | string | `"HH:MM"`. When quiet hours start. Defaults to `21:00`. |
| `quiet_hours_end` | string | `"HH:MM"`. When they end. Defaults to `08:00`. |
| `time_zone` | string | An IANA time zone name. Quiet hours are in each recipient's own time zone where it is known, and in this one otherwise. Defaults to `UTC`. |
| `default_segment_id` | string \| null | A segment of this project: the audience Autopilot plans for by default. |
| `topic_id` | string \| null | A topic of this project for its campaigns. |
| `approval_mode` | string | `ask`: every proposal waits for a person's yes. `earned`: the kinds an admin put on Auto (`PATCH /autopilot/kinds/{kind}`) schedule their campaigns themselves; every other kind still asks. |
| `monthly_credit_cap` | integer | AI credits Autopilot may spend a month, 0–100000. Defaults to 200. More than the project's own monthly AI credits is `422`. |
| `notify_member_ids` | string[] | Up to 20 members of this project, told when proposals are ready. |
| `voice_notes` | string \| null | Your own notes on how your campaigns should sound, up to 500 characters. |

Update Autopilot's settings:

```sh
curl -X PATCH "https://api.rasket.com/autopilot/settings" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": true,
  "store_id": "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
  "channels": ["email", "sms"],
  "time_zone": "America/New_York"
}'
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/settings", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    enabled: true,
    store_id: "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
    channels: ["email", "sms"],
    time_zone: "America/New_York"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/autopilot/settings",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "enabled": True,
    "store_id": "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
    "channels": ["email", "sms"],
    "time_zone": "America/New_York"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "autopilot_settings",
  "enabled": true,
  "store_id": "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
  "channels": ["email", "sms"],
  "kinds_enabled": ["new_arrivals", "back_in_stock", "sale", "blog_post"],
  "max_emails_per_week": 2,
  "max_sms_per_week": 1,
  "min_hours_between_per_contact": 48,
  "quiet_hours_start": "21:00",
  "quiet_hours_end": "08:00",
  "time_zone": "America/New_York",
  "default_segment_id": "0199d2c1-5a32-7b44-9c55-7d6e8f9a0b12",
  "topic_id": null,
  "approval_mode": "ask",
  "monthly_credit_cap": 200,
  "notify_member_ids": ["0199c4e2-1f7a-7b30-8e52-3d9c6a1b2f48"],
  "voice_notes": "Warm and brief. Lead with the product, never with the discount.",
  "updated_at": "2026-09-08T16:30:12.004Z"
}
```

- Admins only: a team admin, or a Full access key an admin created. Anyone else is `403 invalid_permission`, and an OAuth token cannot change the settings.
- An empty body is `422`. A store, segment, topic or member that is not this project's is `422 invalid_parameter`.
- The response is the whole settings object, as saved.

### `GET /autopilot/proposals`

The campaigns Autopilot suggested, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | Only proposals in this status: `drafting`, `proposed`, `approved`, `scheduled`, `sent`, `skipped`, `expired` or `failed`. |
| `kind` | string | Only proposals of this kind: `new_arrivals`, `price_drop`, `back_in_stock`, `low_stock`, `bestsellers`, `seasonal`, `sale` or `blog_post`. |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List proposals:

```sh
curl -X GET "https://api.rasket.com/autopilot/proposals?status=proposed" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/proposals?status=proposed", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/autopilot/proposals?status=proposed",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "autopilot_proposal",
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
      "kind": "new_arrivals",
      "status": "proposed",
      "audience": {
        "segment_id": "0199d2c1-5a32-7b44-9c55-7d6e8f9a0b12",
        "description": "Newsletter subscribers who bought in the last year",
        "recipient_estimate": 4210
      },
      "channels": ["email", "sms"],
      "send_at": "2026-09-11T15:00:00.000Z",
      "email_broadcast_id": "0199d3b1-7b20-7c31-8d42-6e5f7a8b9c01",
      "sms_broadcast_id": "0199d3b1-7b20-7c31-8d42-6e5f7a8b9c02",
      "created_at": "2026-09-09T08:12:44.512Z"
    }
  ]
}
```

- Each item is the whole proposal, brief included; the example is cut short. `status=proposed` is the ones waiting for a decision.
- The list is empty until Autopilot has proposed something.

### `GET /autopilot/proposals/{id}`

One proposal, with its brief, audience and draft campaigns.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The proposal's ID. |

Retrieve a proposal:

```sh
curl -X GET "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "autopilot_proposal",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "kind": "new_arrivals",
  "status": "proposed",
  "store_id": "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
  "signal_ids": ["0199d3b0-2d10-7a44-8c21-4b3a2c1d0e9f"],
  "brief": {
    "kind": "new_arrivals",
    "facts": {
      "products": [
        {
          "id": "8123456789",
          "title": "Linen overshirt",
          "url": "https://shop.acme.example/products/linen-overshirt",
          "image_url": "https://shop.acme.example/images/linen-overshirt.jpg",
          "price": "68.00",
          "previous_price": null,
          "currency": "USD",
          "stock": 42
        }
      ],
      "collection": {
        "id": "4012345678",
        "title": "Autumn arrivals",
        "url": "https://shop.acme.example/collections/autumn"
      },
      "sale": null,
      "blog_post": null,
      "holiday": null
    },
    "angle": "The autumn collection landed this week, led by the linen overshirt.",
    "audience_description": "Newsletter subscribers who bought in the last year",
    "channels": ["email", "sms"],
    "offer": {
      "type": "none"
    },
    "send_at": "2026-09-11T15:00:00.000Z",
    "must_say": [],
    "must_not_say": []
  },
  "audience": {
    "segment_id": "0199d2c1-5a32-7b44-9c55-7d6e8f9a0b12",
    "description": "Newsletter subscribers who bought in the last year",
    "recipient_estimate": 4210
  },
  "channels": ["email", "sms"],
  "send_at": "2026-09-11T15:00:00.000Z",
  "expires_at": null,
  "email_broadcast_id": "0199d3b1-7b20-7c31-8d42-6e5f7a8b9c01",
  "sms_broadcast_id": "0199d3b1-7b20-7c31-8d42-6e5f7a8b9c02",
  "expected": {
    "low": 640,
    "high": 1180,
    "currency": "USD",
    "basis": "Your last three new-arrival campaigns earned $590–$1,240 each."
  },
  "checks": [
    {
      "name": "prices_match_store",
      "passed": true,
      "message": null
    },
    {
      "name": "links_live",
      "passed": true,
      "message": null
    }
  ],
  "judge_score": 8.6,
  "skip_reason": null,
  "skip_note": null,
  "decided_at": null,
  "auto": false,
  "sent_at": null,
  "created_at": "2026-09-09T08:12:44.512Z",
  "updated_at": "2026-09-09T08:13:02.877Z"
}
```

- Every fact in `brief.facts` — product names, prices, stock, links, pictures — comes from your store's catalogue, and `audience.recipient_estimate` is counted by Rasket. None of them comes from the AI.
- `email_broadcast_id` and `sms_broadcast_id` are ordinary draft broadcasts: read or edit them at `/broadcasts/{id}`. Either is `null` when the proposal has no campaign on that channel.
- `expected` is what the campaign is expected to earn, from your store's own history, with `basis` saying how; `null` when there is no history to go on.
- Another project's proposal is `404`.

### `POST /autopilot/proposals/{id}/approve`

Schedule its draft campaigns, through the same checks as a send.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The proposal's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `send_at` | string | A new send time: ISO 8601 or a phrase such as "tomorrow at 9am", read as UTC. Between one minute and thirty days out. Omit it — or send no body at all — to keep the proposal's own `send_at`. |
| `acknowledge_checks` | boolean | Schedule even though a content check fails. Without it, a failing check is `422` with the failing checks in `details.checks`. |

Approve a proposal:

```sh
curl -X POST "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/approve" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "send_at": "2026-09-12T15:00:00Z"
}'
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/approve", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    send_at: "2026-09-12T15:00:00Z"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/approve",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "send_at": "2026-09-12T15:00:00Z"
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "autopilot_proposal",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "status": "scheduled",
  "send_at": "2026-09-12T15:00:00.000Z",
  "decided_at": "2026-09-09T09:40:11.204Z"
}
```

- Needs `broadcasts:write` as well as `autopilot:write`: approving schedules campaigns.
- Each draft goes through the compliance gate and content checks of `POST /broadcasts/{id}/send`, in one transaction. If either draft fails, the approval is refused — `422`, the message saying which campaign, the failing checks in `details.checks` — and nothing changes. A text refused by content policy cannot be acknowledged.
- Once the proposal's own send time has passed, `send_at` is required: without it the call is `409 resource_locked`, asking you to pick a new time.
- Approving a proposal that is already approved or scheduled returns it unchanged. One that was skipped, expired, sent or failed is `409 resource_locked`, and so is one whose drafts were all deleted.
- The response is the whole proposal, now `scheduled`; the example is cut short.

### `POST /autopilot/proposals/{id}/skip`

Say no. Its drafts are deleted and the reason is kept.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The proposal's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `reason` (required) | string | `not_relevant`, `wrong_timing`, `wrong_audience`, `off_brand`, `wrong_facts`, `too_frequent` or `other`. Autopilot learns from it. |
| `note` | string | Anything else, up to 500 characters. |

Skip a proposal:

```sh
curl -X POST "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/skip" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "wrong_timing",
  "note": "We have a sale going out that day."
}'
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/skip", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    reason: "wrong_timing",
    note: "We have a sale going out that day."
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/skip",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "reason": "wrong_timing",
    "note": "We have a sale going out that day."
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "autopilot_proposal",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "status": "skipped",
  "email_broadcast_id": "0199d3b1-7b20-7c31-8d42-6e5f7a8b9c01",
  "sms_broadcast_id": "0199d3b1-7b20-7c31-8d42-6e5f7a8b9c02",
  "skip_reason": "wrong_timing",
  "skip_note": "We have a sale going out that day.",
  "decided_at": "2026-09-09T09:41:30.090Z"
}
```

- Only a `proposed` proposal can be skipped. Skipping one already decided — a second skip included — is `409 resource_locked`.
- The draft broadcasts are deleted, so their IDs answer `404` from then on.
- `skip_reason` can also read `canceled`, which Rasket writes itself when every campaign of an approved proposal is canceled. You cannot send it.

### `POST /autopilot/proposals/{id}/reschedule`

Move its send time before anyone decides.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The proposal's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `send_at` (required) | string | The new send time: ISO 8601 or a phrase such as "tomorrow at 9am", read as UTC. Between one minute and thirty days out. |

Reschedule a proposal:

```sh
curl -X POST "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/reschedule" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "send_at": "2026-09-13T09:00:00Z"
}'
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/reschedule", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    send_at: "2026-09-13T09:00:00Z"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/reschedule",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "send_at": "2026-09-13T09:00:00Z"
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "autopilot_proposal",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "status": "proposed",
  "send_at": "2026-09-13T09:00:00.000Z"
}
```

- Only a proposal waiting for a decision can be rescheduled; one already approved, skipped, expired, sent or failed is `409 resource_locked`. To move an approved campaign, reschedule its broadcast.
- Nothing is scheduled yet: approving afterwards schedules the campaigns at the new time.
- The response is the whole proposal; the example is cut short.

### `POST /autopilot/proposals/{id}/cancel`

Stop a scheduled proposal's campaigns before they send.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The proposal's ID. |

Cancel a proposal:

```sh
curl -X POST "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/cancel" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/cancel", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/proposals/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/cancel",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "autopilot_proposal",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "status": "skipped",
  "skip_reason": "canceled",
  "auto": true
}
```

- For a scheduled proposal, whether it scheduled itself on Auto (`auto: true`) or a person approved it. Its campaigns are canceled and it ends `skipped` with the reason `canceled`, which counts against the kind's readiness.
- Allowed until 30 minutes before `send_at`; after that it is `409 resource_locked`, and the campaign itself can still be stopped with `POST /broadcasts/{id}/cancel`.
- Needs `broadcasts:write` as well as `autopilot:write`. The response is the whole proposal; the example is cut short.

### `GET /autopilot/kinds`

Whether each kind of campaign has earned sending on its own.

List campaign kinds:

```sh
curl -X GET "https://api.rasket.com/autopilot/kinds" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/kinds", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/autopilot/kinds",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "autopilot_kind",
      "kind": "price_drop",
      "enabled": true,
      "auto": false,
      "paused": false,
      "state": "ready",
      "bars": [
        {
          "name": "decided",
          "value": 12,
          "limit": 10,
          "pass": true
        },
        {
          "name": "measured",
          "value": 5,
          "limit": 3,
          "pass": true
        },
        {
          "name": "approved",
          "value": 0.91667,
          "limit": 0.8,
          "pass": true
        },
        {
          "name": "light_edits",
          "value": 0.81818,
          "limit": 0.7,
          "pass": true
        },
        {
          "name": "unsubscribes",
          "value": 0.002,
          "limit": 0.005,
          "pass": true
        },
        {
          "name": "complaints",
          "value": 0.0002,
          "limit": 0.0008,
          "pass": true
        },
        {
          "name": "revenue",
          "value": 1.18,
          "limit": 0.5,
          "pass": true
        }
      ],
      "decided": 12,
      "measured": 5,
      "auto_since": null,
      "last_change": null
    }
  ]
}
```

- All eight kinds, each with its `state` — `auto`, `fell_back`, `not_enough_data`, `not_ready` or `ready` — and the bars it is measured against over the last 90 days: at least 10 decided, 80% approved, 70% of approvals with few edits, unsubscribes at most 0.5%, spam complaints at most 0.08%, and sales (or clicks, for a store with no sales yet) at least half its usual per recipient.
- A kind on Auto is taken off by itself when its results slip, a single campaign draws a complaint spike, or two of its campaigns are canceled; `last_change` says why. It cannot be Ready again for 7 days.

### `PATCH /autopilot/kinds/{kind}`

Let a Ready kind schedule its campaigns itself, or take it off.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `kind` (required) | string | The kind of campaign: `new_arrivals`, `price_drop`, `back_in_stock`, `low_stock`, `bestsellers`, `seasonal`, `sale` or `blog_post`. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `auto` (required) | boolean | `true` puts the kind on Auto; `false` takes it off. |

Put a kind on Auto:

```sh
curl -X PATCH "https://api.rasket.com/autopilot/kinds/{kind}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "auto": true
}'
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/kinds/{kind}", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    auto: true
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/autopilot/kinds/{kind}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "auto": True
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "autopilot_kind",
  "kind": "price_drop",
  "enabled": true,
  "auto": true,
  "paused": false,
  "state": "auto",
  "bars": [
    {
      "name": "decided",
      "value": 12,
      "limit": 10,
      "pass": true
    },
    {
      "name": "measured",
      "value": 5,
      "limit": 3,
      "pass": true
    },
    {
      "name": "approved",
      "value": 0.91667,
      "limit": 0.8,
      "pass": true
    },
    {
      "name": "light_edits",
      "value": 0.81818,
      "limit": 0.7,
      "pass": true
    },
    {
      "name": "unsubscribes",
      "value": 0.002,
      "limit": 0.005,
      "pass": true
    },
    {
      "name": "complaints",
      "value": 0.0002,
      "limit": 0.0008,
      "pass": true
    },
    {
      "name": "revenue",
      "value": 1.18,
      "limit": 0.5,
      "pass": true
    }
  ],
  "decided": 12,
  "measured": 5,
  "auto_since": "2026-09-12T10:00:00.000Z",
  "last_change": null
}
```

- A team admin's. `auto: true` is `422` unless the kind is `ready` (the failing bars are in `details.bars`), enabled in `kinds_enabled`, and `approval_mode` is `earned`.
- On Auto, a proposal of the kind whose checks and quality score passed schedules itself when its send is at least four hours away, and the people Autopilot tells get an email with a Cancel link.

### `GET /autopilot/playbook`

What Autopilot learned from this store's results and edits.

Retrieve the playbook:

```sh
curl -X GET "https://api.rasket.com/autopilot/playbook" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/playbook", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/autopilot/playbook",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "autopilot_playbook",
  "live": null,
  "proposed": {
    "object": "autopilot_playbook_version",
    "id": "0199e1a4-2c30-7d41-8e52-6f7a8b9c0d13",
    "version": 3,
    "status": "proposed",
    "source": "learned",
    "store_id": "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
    "kind_weights": {
      "price_drop": 1.7,
      "bestsellers": 0.83,
      "new_arrivals": 0.96
    },
    "best_hours": {
      "time_zone": "America/New_York",
      "email": [10],
      "sms": []
    },
    "style_notes": ["Lead with the product's material, not with a greeting."],
    "avoid_phrases": ["don't miss out"],
    "prefer_phrases": ["made to last"],
    "suggested_caps": null,
    "evidence": {
      "currency": "USD",
      "channel": "email",
      "baseline_revenue_per_recipient": 0.2,
      "baseline_click_rate": 0.05,
      "kinds": [
        {
          "kind": "price_drop",
          "campaigns": 3,
          "recipients": 300,
          "revenue_per_recipient": 0.6,
          "click_rate": 0.05,
          "skips": 0,
          "decided": 3,
          "performance": 1.7,
          "weight": 1.7
        }
      ],
      "hours": {
        "email": [
          {
            "hour": 10,
            "sends": 6,
            "click_rate": 0.05
          }
        ],
        "sms": []
      },
      "edits": {
        "total": 6,
        "changed": 4,
        "heavy": 1
      },
      "too_frequent_skips": 0
    },
    "based_on": {
      "outcomes": 9,
      "edits": 6,
      "skips": 1,
      "since": "2026-07-01T05:24:00.000Z",
      "until": "2026-09-29T05:24:00.000Z"
    },
    "restored_from_id": null,
    "decided_at": null,
    "created_at": "2026-09-29T05:24:03.117Z"
  },
  "history": [
    {
      "object": "autopilot_playbook_version",
      "id": "0199e1a4-2c30-7d41-8e52-6f7a8b9c0d13",
      "version": 3,
      "status": "proposed",
      "source": "learned",
      "store_id": "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
      "kind_weights": {
        "price_drop": 1.7,
        "bestsellers": 0.83,
        "new_arrivals": 0.96
      },
      "best_hours": {
        "time_zone": "America/New_York",
        "email": [10],
        "sms": []
      },
      "style_notes": ["Lead with the product's material, not with a greeting."],
      "avoid_phrases": ["don't miss out"],
      "prefer_phrases": ["made to last"],
      "suggested_caps": null,
      "evidence": {
        "currency": "USD",
        "channel": "email",
        "baseline_revenue_per_recipient": 0.2,
        "baseline_click_rate": 0.05,
        "kinds": [
          {
            "kind": "price_drop",
            "campaigns": 3,
            "recipients": 300,
            "revenue_per_recipient": 0.6,
            "click_rate": 0.05,
            "skips": 0,
            "decided": 3,
            "performance": 1.7,
            "weight": 1.7
          }
        ],
        "hours": {
          "email": [
            {
              "hour": 10,
              "sends": 6,
              "click_rate": 0.05
            }
          ],
          "sms": []
        },
        "edits": {
          "total": 6,
          "changed": 4,
          "heavy": 1
        },
        "too_frequent_skips": 0
      },
      "based_on": {
        "outcomes": 9,
        "edits": 6,
        "skips": 1,
        "since": "2026-07-01T05:24:00.000Z",
        "until": "2026-09-29T05:24:00.000Z"
      },
      "restored_from_id": null,
      "decided_at": null,
      "created_at": "2026-09-29T05:24:03.117Z"
    }
  ]
}
```

- `live` is the version Autopilot plans and writes with; `proposed` is the one waiting for an admin. Each is `null` until there is one, and `history` lists the ten newest versions.
- `kind_weights` multiply the planner's own weight for each kind, between 0.25 and 2.0. Every number — weights, hours, the evidence — is worked out from the store's own campaign results; only `style_notes` and the phrases are written by AI, from the edits made before approving.
- A campaign that uses one of the live version's `avoid_phrases` fails its checks and is rewritten or dropped.

### `POST /autopilot/playbook/versions/{id}/use`

Make the waiting version the one Autopilot follows.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The playbook version's ID. |

Use a proposed version:

```sh
curl -X POST "https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/use" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/use", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/use",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "autopilot_playbook",
  "live": {
    "object": "autopilot_playbook_version",
    "id": "0199e1a4-2c30-7d41-8e52-6f7a8b9c0d13",
    "version": 3,
    "status": "live"
  },
  "proposed": null
}
```

- A team admin's, or a Full access key an admin created; anyone else is `403 invalid_permission`.
- The version that was live is archived. Only the waiting version can be used: once a newer one replaced it, the call is `409 resource_locked`.
- The response is the whole playbook; the example is cut short.

### `POST /autopilot/playbook/versions/{id}/dismiss`

Say no to the waiting version; what is live stays.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The playbook version's ID. |

Dismiss a proposed version:

```sh
curl -X POST "https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/dismiss" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/dismiss", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/dismiss",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "autopilot_playbook",
  "live": null,
  "proposed": null
}
```

- A team admin's. The version is kept as `rejected`, and Autopilot does not propose the same again until at least three more campaigns' results are in.
- Only the waiting version can be dismissed: `409 resource_locked` otherwise.

### `POST /autopilot/playbook/versions/{id}/restore`

Go back to an earlier version.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The playbook version's ID. |

Restore a version:

```sh
curl -X POST "https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/restore" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/restore", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/playbook/versions/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/restore",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "autopilot_playbook",
  "live": {
    "object": "autopilot_playbook_version",
    "version": 4,
    "status": "live",
    "source": "restored",
    "restored_from_id": "0199d9f0-1b20-7c31-8d42-5e6f7a8b9c02"
  }
}
```

- A team admin's. A copy of the version becomes the live one, as the next version number, with `source: "restored"`; the version live before it is archived. A version waiting for a decision stays waiting.
- Restoring the version that is already live is `409 resource_locked`; an unknown ID is `404`.

### `POST /autopilot/playbook/reset`

Stop following any learned version.

Reset to the defaults:

```sh
curl -X POST "https://api.rasket.com/autopilot/playbook/reset" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/playbook/reset", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/playbook/reset",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "autopilot_playbook",
  "live": null
}
```

- A team admin's. The live version is archived, so Autopilot plans and writes with its defaults until a version is used again. With nothing live it changes nothing.

### `GET /autopilot/sources`

The blog and RSS feeds Autopilot reads for its store.

List feeds:

```sh
curl -X GET "https://api.rasket.com/autopilot/sources" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/sources", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/autopilot/sources",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "store_id": "0199d3a0-3b10-7c22-9d41-5e2f6a7b8c91",
  "shopify": false,
  "needs_blog_permission": false,
  "shopify_apps_url": null,
  "data": [
    {
      "object": "autopilot_source",
      "id": "0199d3c4-2a10-7d22-8e41-6f2a7b8c9d03",
      "kind": "rss",
      "url": "https://journal.acme.example/feed",
      "title": "The Acme Journal",
      "status": "active",
      "last_error": null,
      "last_fetched_at": "2026-09-10T07:23:04.118Z",
      "latest": {
        "title": "How we dye linen",
        "url": "https://journal.acme.example/how-we-dye-linen",
        "published_at": "2026-09-09T12:00:00.000Z"
      }
    }
  ]
}
```

- The feeds belong to the store Autopilot watches (`store_id` in the settings). A new post on one becomes a `blog_post` proposal.
- A Shopify store's own blog is read without being added, once the store has approved the app's `read_content` permission; until then `needs_blog_permission` is `true` and `shopify_apps_url` opens Rasket in the store's admin, where Shopify asks for it. Both are `false` and `null` for any other store.

### `POST /autopilot/sources`

Read an RSS or Atom feed once and watch it for new posts.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `url` (required) | string | The feed's `https://` address, such as `https://journal.example.com/feed`. A web page is refused: feeds usually end in `/feed`, `/rss` or `.xml`. |

Add a feed:

```sh
curl -X POST "https://api.rasket.com/autopilot/sources" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://journal.acme.example/feed"
}'
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/sources", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://journal.acme.example/feed"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/autopilot/sources",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "url": "https://journal.acme.example/feed"
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "autopilot_source",
  "id": "0199d3c4-2a10-7d22-8e41-6f2a7b8c9d03",
  "kind": "rss",
  "url": "https://journal.acme.example/feed",
  "title": "The Acme Journal",
  "status": "active",
  "last_error": null,
  "last_fetched_at": "2026-09-10T07:23:04.118Z",
  "latest": {
    "title": "How we dye linen",
    "url": "https://journal.acme.example/how-we-dye-linen",
    "published_at": "2026-09-09T12:00:00.000Z"
  }
}
```

- Rasket reads the feed before adding it: https only, public addresses only, at most three redirects, 1 MB and 10 seconds. What it shows then is the baseline, so only posts published afterwards become proposals.
- A team admin's, or a Full access key an admin made. At most five feeds per store (`422`), and twenty checks an hour per project (`429 rate_limited`).
- A feed that cannot be read is `422` with the reason in `message`.

### `DELETE /autopilot/sources/{id}`

Stop reading a feed.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The feed's ID. |

Remove a feed:

```sh
curl -X DELETE "https://api.rasket.com/autopilot/sources/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/autopilot/sources/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/autopilot/sources/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "autopilot_source",
  "id": "0199d3c4-2a10-7d22-8e41-6f2a7b8c9d03",
  "deleted": true
}
```

- Its posts not yet proposed are no longer news. The Shopify blog is not a feed you can remove: switch off the `blog_post` kind instead.
