# 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. Too few AI credits for the request: `422 validation_error`, "AI credits for this month are used up." or "This needs 3 AI credits and 1 is left this month.", with `details.reason` `insufficient_credits` and `details.needed` and `details.available`. 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.

## AI credits

Every plan includes AI credits each month: Free 5, Pro 100, Scale and Enterprise 500. Purchased credits are spent once the month's allowance is used up, soonest to expire first, and last 12 months. Autopilot only ever spends the allowance. An action starts only when its whole price is available, and a failed or refused request, the help chat and a diagnosis read back cost nothing. The team's admins are emailed at 80% and 100% of the allowance unless `ai_credit_alerts` is off.

| What costs credits | Credits |
| --- | --- |
| Enhance (Inbox) | 1 |
| Draft reply (Inbox) | 1 |
| Rewrite a block | 1 |
| Alt text | 1 |
| Subject-line ideas | 1 |
| A/B subjects | 1 |
| Draft an email (HTML) | 1 |
| Draft an email (editor) | 3 |
| Create a form with AI (3 drafts) | 3, plus 4 per AI picture |
| AI picture (reused pictures free) | 4 |
| Autopilot campaign (write + check) | 4, plus 4 per AI picture |
| Autopilot weekly style proposal | 1 |
| Diagnose an email (once per change) | 1 |

`GET /ai/credits` returns the balance and this price list, `GET /ai/usage` where the credits went by feature, member or day, and `GET /ai/activity` every request of the last 13 months — never the text sent to the AI.

## 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,
    "purchased_remaining": 0
  }
}
```

- 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`, too few AI credits is `422 validation_error` with `details.reason: "insufficient_credits"`, `details.needed` and `details.available`, 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`, too few AI credits is `422 validation_error` with `details.reason: "insufficient_credits"`, `details.needed` and `details.available`, 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`, too few AI credits is `422 validation_error` with `details.reason: "insufficient_credits"`, `details.needed` and `details.available`, 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`, too few AI credits is `422 validation_error` with `details.reason: "insufficient_credits"`, `details.needed` and `details.available`, 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`, too few AI credits is `422 validation_error` with `details.reason: "insufficient_credits"`, `details.needed` and `details.available`, 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`, too few AI credits is `422 validation_error` with `details.reason: "insufficient_credits"`, `details.needed` and `details.available`, 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 /ai/draft-forms`

Three sign-up form drafts from a description, in your brand, ready to save as a form's draft.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `prompt` (required) | string | The form you want in a sentence or more, up to 2,000 characters: what it is for, what it offers and what it asks. The drafts are in its language. |
| `kind` | string | `popup`, `flyout` or `embedded`: make all three drafts this kind. Omitted, each suits the ask. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `locale` | string | Write the drafts in this language, such as `de` or `fr-CA`. |

Draft sign-up forms:

```sh
curl -X POST "https://api.rasket.com/ai/draft-forms" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "prompt": "A pop-up for our fall drop. 10% off for email, phone too if they want texts. Earthy colours, show one of the new jackets."
}'
```

```ts
const response = await fetch("https://api.rasket.com/ai/draft-forms", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    prompt: "A pop-up for our fall drop. 10% off for email, phone too if they want texts. Earthy colours, show one of the new jackets."
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/ai/draft-forms",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "prompt": "A pop-up for our fall drop. 10% off for email, phone too if they want texts. Earthy colours, show one of the new jackets."
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_form_drafts",
  "drafts": [
    {
      "angle": "classic",
      "name": "Classic offer",
      "description": "Email + phone, 10% off",
      "kind": "popup",
      "notes": [],
      "needs": ["coupon_code"],
      "properties": [],
      "document": {
        "steps": [],
        "style": {},
        "targeting": {},
        "triggers": {}
      }
    }
  ],
  "partial": false,
  "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`, too few AI credits is `422 validation_error` with `details.reason: "insufficient_credits"`, `details.needed` and `details.available`, and more than 10 AI requests a minute is `429 rate_limit_exceeded`.
- Each draft is laid out on one of the form templates in your brand kit's colours or your store theme's, with your logo when the kit has one. A product you ask for comes from your Shopify catalogue, and a phone field carries the standard text-message consent wording with the terms and privacy links from your SMS registration.
- Every `document` passes the checks Publish runs except what only you can supply, listed in `needs`: the discount code a coupon reveals. Create any contact properties in `properties` before publishing. Nothing is saved.
- An accepted request spends 3 credits; a failed one spends nothing. A description of fewer than 3 words is `422 validation_error`.

### `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,
    "purchased_remaining": 0
  }
}
```

- 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`, too few AI credits is `422 validation_error` with `details.reason: "insufficient_credits"`, `details.needed` and `details.available`, 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.

### `GET /ai/credits`

This month's AI credits, purchased credits, and what every AI action costs.

Retrieve AI credits:

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

```ts
const response = await fetch("https://api.rasket.com/ai/credits", {
  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/ai/credits",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_credits",
  "limit": 100,
  "used": 72,
  "remaining": 28,
  "allowance_remaining": 28,
  "purchased_remaining": 0,
  "purchased_expires_at": null,
  "period_start": "2026-10-01T00:00:00.000Z",
  "resets_at": "2026-11-01T00:00:00.000Z",
  "autopilot": {
    "cap": 100,
    "spent": 8
  },
  "alerts": true,
  "prices": [
    {
      "action": "inbox_enhance",
      "feature": "inbox",
      "label": "Enhance (Inbox)",
      "credits": 1,
      "per_picture": null
    },
    {
      "action": "create_form",
      "feature": "forms",
      "label": "Create a form with AI (3 drafts)",
      "credits": 3,
      "per_picture": 4
    }
  ]
}
```

- Purchased credits are spent once the month's allowance is used up, soonest to expire first. Autopilot only ever spends the allowance.
- `remaining` is what the team can spend right now, less AI requests still running. Failed or refused requests and the help chat never cost credits. Reading this costs nothing.

### `GET /ai/usage`

Where the team's AI credits went, by feature, member or day.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `group_by` | string | `feature` (the default), `member` or `day` (UTC). |
| `from` | string | ISO 8601, inclusive. Default: the start of this month. |
| `to` | string | ISO 8601, exclusive. Default: the start of next month. At most 13 months after `from`. |

Retrieve AI usage:

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

```ts
const response = await fetch("https://api.rasket.com/ai/usage", {
  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/ai/usage",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_usage",
  "group_by": "feature",
  "from": "2026-10-01T00:00:00.000Z",
  "to": "2026-11-01T00:00:00.000Z",
  "total": 72,
  "data": [
    {
      "key": "inbox",
      "label": null,
      "credits": 31
    },
    {
      "key": "editor",
      "label": null,
      "credits": 18
    },
    {
      "key": "forms",
      "label": null,
      "credits": 15
    },
    {
      "key": "autopilot",
      "label": null,
      "credits": 8
    }
  ]
}
```

- Totals are the ledger's: every credit charged counts once. By member, `key` is a user ID, `autopilot`, `api` (an API key, or a member since removed) or `unknown` (a request older than the activity log).

### `GET /ai/activity`

The team's AI requests of the last 13 months, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | number | 1–100, default 20. |
| `after` | string | Return requests older than this ID. |
| `before` | string | Return requests newer than this ID. |
| `feature` | string | `inbox`, `editor`, `forms`, `pictures`, `autopilot` or `emails`. |
| `user_id` | string | Only this member's requests. |

List AI activity:

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

```ts
const response = await fetch("https://api.rasket.com/ai/activity", {
  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/ai/activity",
    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": "ai_activity",
      "id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "created_at": "2026-10-27T14:02:00.000Z",
      "user_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c",
      "actor": "member",
      "feature": "inbox",
      "kind": "enhance_reply",
      "status": "ok",
      "target_type": null,
      "target_id": null,
      "credits": 1
    }
  ]
}
```

- Who, what, when and what it cost — never the text sent to the AI or what it answered.

### `POST /ai/credits/requests`

Email the team's admins that you need more AI credits.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `action` | string | The AI action that did not fit, such as `create_form`. Named in the email. |

Ask for more AI credits:

```sh
curl -X POST "https://api.rasket.com/ai/credits/requests" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "action": "create_form"
}'
```

```ts
const response = await fetch("https://api.rasket.com/ai/credits/requests", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    action: "create_form"
  }),
});

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

```python
import os

import requests

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

print(response.json())
```

#### Response `200`

```json
{
  "object": "ai_credit_request",
  "status": "sent",
  "admins": 2
}
```

- Only an admin can buy credits, so a key that acts as the team's admin is `403 invalid_permission`.
- Once in 24 hours per caller and five times a day per team; a repeat is `status: "already_asked"` and sends nothing. Costs nothing.
