# AI

Subject lines, a first draft, and an explanation of what happened to an email — the dashboard's three AI helpers, over the API — and the visual editor's four: a draft as blocks, a rewritten block, alt text, and subject variants for an A/B test.

## Who can call it

| Credential | Every AI route |
| --- | --- |
| `full_access` | 200 |
| `sending_access` | `401 restricted_api_key` |
| OAuth token with `ai:use` | 200 |
| OAuth token without `ai:use` | `403 invalid_permission` |

What is sent to the model and what is kept is described in the [AI assist guide](https://www.rasket.com/docs/ai). A recipient's address is never part of it.

## Refusals, in order

A request that passes authentication is checked in this order, so the first to fail is the one you see:

1. AI assist is not configured for the account: `503 service_unavailable`, "AI assist is not available on this account." Nothing is recorded and no credit is spent; there is nothing to turn on at your end.
2. AI assist is off for the team: `403 invalid_permission`, "AI assist is off for this team. A team admin can turn it on in Settings." Turn it on with `PATCH /team` and `ai_assist_enabled: true`.
3. The month's credits are spent: `422 validation_error`, "AI credits for this month are used up." Every answer carries `credits_remaining`, so you can stop before this.
4. More than 10 AI requests in a minute, per team: `429 rate_limit_exceeded`.

An accepted request spends one credit, and a draft as blocks three. A request that fails on our side costs nothing, and a diagnosis read back for an email that has not changed costs nothing either. Nor does a diagnosis of an email that failed on our mail servers' side: it comes back with `cause: "provider_failure"` and the recorded reason.

## Endpoints

### `POST /ai/subject-lines`

Subject lines for an email you describe. Nothing is saved.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `subject` | string | The current subject, if any. |
| `html` | string | The HTML body, up to 100,000 characters. |
| `text` | string | The plain-text body, up to 100,000 characters. |
| `audience` | string | Who the email is for, up to 2,000 characters. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `template_id` | string | One of the team's templates, recorded as what the request was about. The content still comes from this body. |

Suggest subject lines:

```sh
curl -X POST "https://api.rasket.com/ai/subject-lines" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "subject": "Our November update",
  "text": "Three new features and a price freeze.",
  "audience": "Customers on the Pro plan"
}'
```

```ts
const response = await fetch("https://api.rasket.com/ai/subject-lines", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    subject: "Our November update",
    text: "Three new features and a price freeze.",
    audience: "Customers on the Pro plan"
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/ai/subject-lines",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "subject": "Our November update",
    "text": "Three new features and a price freeze.",
    "audience": "Customers on the Pro plan"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_subject_suggestions",
  "suggestions": ["Three new features, and your price stays put", "November: what's new on your Pro plan"],
  "credits_remaining": 87,
  "credits": {
    "limit": 100,
    "used": 13,
    "remaining": 87
  }
}
```

- Refused in this order: AI assist not configured for the account is `503 service_unavailable` and costs nothing, AI assist off for the team is `403 invalid_permission`, the month's credits spent is `422 validation_error`, and more than 10 AI requests a minute is `429 rate_limit_exceeded`.
- An accepted request spends one credit.

### `POST /ai/draft`

An HTML and plain-text body from a brief, or a rework of a draft.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `brief` (required) | string | What the email should say, up to 2,000 characters. |
| `tone` | string | `neutral`, `friendly` or `formal`. |
| `existing_html` | string | A draft to rework instead of starting fresh, up to 100,000 characters. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `template_id` | string | One of the team's templates, recorded as what the request was about. |

Draft an email body:

```sh
curl -X POST "https://api.rasket.com/ai/draft" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "brief": "Welcome a new customer and point them at the quickstart.",
  "tone": "friendly"
}'
```

```ts
const response = await fetch("https://api.rasket.com/ai/draft", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    brief: "Welcome a new customer and point them at the quickstart.",
    tone: "friendly"
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/ai/draft",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "brief": "Welcome a new customer and point them at the quickstart.",
    "tone": "friendly"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_draft",
  "html": "<p>Welcome aboard! …</p>",
  "text": "Welcome aboard! …",
  "credits_remaining": 86,
  "credits": {
    "limit": 100,
    "used": 14,
    "remaining": 86
  }
}
```

- Refused in this order: AI assist not configured for the account is `503 service_unavailable` and costs nothing, AI assist off for the team is `403 invalid_permission`, the month's credits spent is `422 validation_error`, and more than 10 AI requests a minute is `429 rate_limit_exceeded`.
- There is no subject in the answer: ask for subject lines separately. Nothing is saved or sent.

### `POST /ai/draft-blocks`

A block document from a brief, in your brand kit, ready to save as a template's `design`.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `brief` (required) | string | What the email should say, up to 2,000 characters. The draft is in the brief's language. |
| `goal` | string | What the email is for, such as `announcement` (the default), `promotion`, `newsletter`, `welcome`, `event`, `product_update` or `feedback`. |
| `tone` | string | `neutral` (the default), `friendly` or `formal`. |
| `length` | string | `short`, `medium` (the default) or `long`. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `starter_hint` | string | A starter's name to borrow the shape of, up to 500 characters. |
| `template_id` | string | One of the team's templates, recorded as what the request was about. |

Draft an email as blocks:

```sh
curl -X POST "https://api.rasket.com/ai/draft-blocks" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "brief": "Announce our new reporting dashboard to existing customers.",
  "goal": "product_update"
}'
```

```ts
const response = await fetch("https://api.rasket.com/ai/draft-blocks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    brief: "Announce our new reporting dashboard to existing customers.",
    goal: "product_update"
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/ai/draft-blocks",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "brief": "Announce our new reporting dashboard to existing customers.",
    "goal": "product_update"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_block_draft",
  "document": {
    "kind": "rasket.blocks",
    "version": 1,
    "lang": "en",
    "sections": []
  },
  "credits_remaining": 84,
  "credits": {
    "limit": 100,
    "used": 16,
    "remaining": 84
  }
}
```

- Refused in this order: AI assist not configured for the account is `503 service_unavailable` and costs nothing, AI assist off for the team is `403 invalid_permission`, the month's credits spent is `422 validation_error`, and more than 10 AI requests a minute is `429 rate_limit_exceeded`.
- The layout is your brand kit's tokens, with your logo when the kit has one, and the document ends in a footer with an unsubscribe link. Nothing is saved or sent.
- An accepted request spends 3 credits; a failed one spends nothing.

### `POST /ai/rewrite`

One heading, text or button block, rewritten. Nothing is saved.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `block` (required) | object | The heading, text or button block, as it is in the template's `design`. Its id, type, style, links and `{{variables}}` are kept; only its text changes. |
| `preset` | string | `shorter`, `longer`, `friendlier` or `formal`. Send this or `instruction`. |
| `instruction` | string | How to rewrite it, in your words, up to 500 characters. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `template_id` | string | One of the team's templates, recorded as what the request was about. |

Rewrite a block:

```sh
curl -X POST "https://api.rasket.com/ai/rewrite" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "block": {
    "id": "h",
    "type": "heading",
    "level": 1,
    "runs": [
      {
        "text": "Our new reporting dashboard"
      }
    ]
  },
  "preset": "shorter"
}'
```

```ts
const response = await fetch("https://api.rasket.com/ai/rewrite", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    block: {
      id: "h",
      type: "heading",
      level: 1,
      runs: [
        {
          text: "Our new reporting dashboard"
        }
      ]
    },
    preset: "shorter"
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/ai/rewrite",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "block": {
      "id": "h",
      "type": "heading",
      "level": 1,
      "runs": [
        {
          "text": "Our new reporting dashboard"
        }
      ]
    },
    "preset": "shorter"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_block_rewrite",
  "block": {
    "id": "h",
    "type": "heading",
    "level": 1,
    "runs": [
      {
        "text": "New: reporting"
      }
    ]
  },
  "credits_remaining": 83,
  "credits": {
    "limit": 100,
    "used": 17,
    "remaining": 83
  }
}
```

- Refused in this order: AI assist not configured for the account is `503 service_unavailable` and costs nothing, AI assist off for the team is `403 invalid_permission`, the month's credits spent is `422 validation_error`, and more than 10 AI requests a minute is `429 rate_limit_exceeded`.
- An accepted request spends one credit; a failed one spends nothing.

### `POST /ai/alt-text`

One sentence of alt text for a picture. Nothing is saved.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `image_url` | string | The picture's `https://` address, which the assistant's provider fetches to look at. An address with a `{{variable}}` in it is not sent; describe the picture instead. |
| `description` | string | What the picture shows, up to 500 characters. Required without `image_url`. |
| `context` | string | The text around the picture, up to 2,000 characters. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `template_id` | string | One of the team's templates, recorded as what the request was about. |

Suggest alt text:

```sh
curl -X POST "https://api.rasket.com/ai/alt-text" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "description": "A bar chart of weekly sends rising over a month",
  "context": "Our new reporting dashboard"
}'
```

```ts
const response = await fetch("https://api.rasket.com/ai/alt-text", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    description: "A bar chart of weekly sends rising over a month",
    context: "Our new reporting dashboard"
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/ai/alt-text",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "description": "A bar chart of weekly sends rising over a month",
    "context": "Our new reporting dashboard"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_alt_text",
  "alt": "A bar chart showing weekly sends rising over one month.",
  "credits_remaining": 82,
  "credits": {
    "limit": 100,
    "used": 18,
    "remaining": 82
  }
}
```

- Refused in this order: AI assist not configured for the account is `503 service_unavailable` and costs nothing, AI assist off for the team is `403 invalid_permission`, the month's credits spent is `422 validation_error`, and more than 10 AI requests a minute is `429 rate_limit_exceeded`.
- An accepted request spends one credit; a failed one spends nothing.

### `POST /ai/subject-variants`

Three subject lines for an A/B test, each a different approach. Nothing is saved.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `subject` | string | The current subject, if any. |
| `html` | string | The HTML body, up to 100,000 characters. |
| `text` | string | The plain-text body, up to 100,000 characters. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `broadcast_id` | string | The campaign the variants are for, recorded as what the request was about. Nothing is saved on it. |
| `template_id` | string | One of the team's templates, recorded as what the request was about. |

Suggest subject variants:

```sh
curl -X POST "https://api.rasket.com/ai/subject-variants" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "subject": "Our November update",
  "text": "Three new features and a price freeze."
}'
```

```ts
const response = await fetch("https://api.rasket.com/ai/subject-variants", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    subject: "Our November update",
    text: "Three new features and a price freeze."
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/ai/subject-variants",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "subject": "Our November update",
    "text": "Three new features and a price freeze."
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_subject_variants",
  "variants": ["Three new features, same price", "What's new in November", "Your plan just got better"],
  "credits_remaining": 81,
  "credits": {
    "limit": 100,
    "used": 19,
    "remaining": 81
  }
}
```

- Refused in this order: AI assist not configured for the account is `503 service_unavailable` and costs nothing, AI assist off for the team is `403 invalid_permission`, the month's credits spent is `422 validation_error`, and more than 10 AI requests a minute is `429 rate_limit_exceeded`.
- Keep the ones you want with `subject_variants` on `PATCH /broadcasts/{id}`. An accepted request spends one credit; a failed one spends nothing.

### `POST /emails/{email_id}/diagnose`

What happened to one email, its likely causes, and what to do next.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `email_id` (required) | string | The email's ID. |

Diagnose an email:

```sh
curl -X POST "https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/diagnose" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/diagnose", {
  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/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c/diagnose",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_diagnosis",
  "summary": "Email 4ef9a417-02e9-4d39-ad75-9611e0fcc33c bounced permanently at one recipient domain.",
  "likely_causes": ["The mailbox does not exist at the receiving domain."],
  "next_steps": ["Remove the address, or confirm it with the recipient."],
  "cause": null,
  "created_at": "2026-09-12T10:00:00.000Z",
  "stored": false,
  "credits_remaining": 85,
  "credits": {
    "limit": 100,
    "used": 15,
    "remaining": 85
  }
}
```

- Refused in this order: AI assist not configured for the account is `503 service_unavailable` and costs nothing, AI assist off for the team is `403 invalid_permission`, the month's credits spent is `422 validation_error`, and more than 10 AI requests a minute is `429 rate_limit_exceeded`.
- The model sees recipient domains, statuses and the event timeline — never a recipient's address or the body.
- A diagnosis already made for the same state of the email comes back with `stored: true` and costs nothing.
- An email that failed on our mail servers' side comes back with `cause: "provider_failure"`: the recorded reason and what to do, with no credit spent.
