> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rippit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Conversations

> Send conversations from your own systems into Rippit: chats, tickets, emails, calls, bot transcripts, AI agent traces, etc.

A conversation is an ordered set of messages, usually between a customer and an agent (a support rep, a sales rep, or a bot). It can also be one-sided, such as an AI agent trace where every step is the agent's. Each upload line is one conversation and all of its messages.

You don't have to write the code yourself: drop the [Bulk Ingest API skill](https://docs.rippit.com/ingest-api/rippit-bulk-ingest-api.md) into your assistant and it walks you through the upload. Assistants can read this guide as raw markdown at [bulk\_conversations.md](https://integrations.maestroqa.com/api/v2/guides/bulk_conversations.md).

## How it works

1. **Name the conversation source once**, such as `in-app chat`.
2. **Ask for an upload slot** for it. Rippit returns a one-time upload link.
3. **Upload one file** to that link. The file has one conversation per line.
4. **Tell Rippit the upload is finished.** Processing starts right away.
5. **Check the job** until it says `done`. Any rows that failed come back with their line number and the reason.

Uploading the same conversation again updates it instead of creating a duplicate. That makes it safe to run the same export every hour or every day.

### Conversation sources

A **source** is where a set of conversations comes from, such as `in-app chat`, `support email` or `sales calls`. You name it once, then pass that name as `source` on every upload. Each source becomes its own table in Rippit, with one row per conversation and your `searchable` keys as columns.

A source's columns are the `searchable` keys of its first upload. Later uploads should use the same keys: a file whose keys fit another of your sources better than the one you named fails with a message naming the better fit, so a sales-call export can't end up in your support chats by mistake.

To add your own tables alongside, such as accounts or orders, see the [bulk dataset upload](https://docs.rippit.com/ingest-api/tables.md). Join them to conversations on `conversation_id`.

## Before you start

**Get an API token.** In Rippit, open **Tables**, click **Add table**, then **Add API Table**, and create or copy a token. You need to be a workspace admin. Keep it in an environment variable such as `RIPPIT_API_TOKEN`, not in a script or a chat.

**Authentication.** Send your token in the `apitoken` header on every request:

```python theme={null}
headers = {'apitoken': 'YOUR_API_TOKEN'}
requests.post(url, headers=headers)
```

**Base URL:** `https://integrations.maestroqa.com/api/v2/bulk_ingest`

Bulk upload has to be switched on for your account. If it isn't, requests return `403` with "Your account does not have API ingest enabled." Ask your Rippit contact to turn it on.

## The file format

Upload **JSON Lines** (`.jsonl`): one conversation per line, and each line is a complete JSON object.

```json theme={null}
{"conversation": {"conversation_id": "chat_8f2a", "started_at": "2026-09-27T14:02:11Z", "ended_at": "2026-09-27T14:09:40Z", "searchable": {"channel": "web_chat", "plan": "pro", "csat": 5}}, "events": [{"event_id": "1", "time": "2026-09-27T14:02:11Z", "author_role": "customer", "author_email": "sam@acme.com", "author_name": "Sam Lee", "body": "My invoice is wrong"}, {"event_id": "2", "time": "2026-09-27T14:03:05Z", "author_role": "agent", "author_id": "agent_42", "author_name": "Alex Kim", "body": "Sorry about that, let me check."}]}
```

### `conversation`

| Field | Required | What it is |
| - | - | - |
| `conversation_id` | yes | Your ID for the conversation. Maximum 96 characters. Reuse the same ID to update a conversation. |
| `started_at` | yes | When the conversation started, as an ISO 8601 timestamp with a timezone, for example `2026-09-27T14:02:11Z`. |
| `ended_at` | no | When it ended or was last updated. It can't be earlier than `started_at`. If you leave it out, the time of the last event is used. |
| `searchable` | no | Your own fields for filtering and reporting, such as channel, plan, tags or CSAT. At most 50 keys. Values can be text, numbers, `true`/`false` or `null`. |

### `events` (the messages)

| Field | Required | What it is |
| - | - | - |
| `event_id` | yes | Unique within this conversation. Message order or position works well (`"1"`, `"2"` and so on). Maximum 96 characters. |
| `time` | yes | When the message was sent, as an ISO 8601 timestamp. |
| `author_role` | yes | `"agent"` or `"customer"`. |
| `body` | yes | The message text. Maximum 100,000 characters. |
| `author_id` | no | Your ID for the person who wrote the message. |
| `author_email` | no | Their email address. |
| `author_name` | no | Their name, as shown in Rippit. Without it the message shows as "Anonymous". |
| `public` | no | `false` for internal notes. The default is `true`. |
| `attributes` | no | Your own fields on the message. At most 20 keys. |

A conversation can have up to 10,000 events.

**Rules for keys in `searchable` and `attributes`:** letters, numbers and underscores only, starting with a letter or underscore, and at most 64 characters. `plan_tier` works. `Plan Tier!` doesn't.

### More examples

These are pretty-printed so they're easier to read. In your file, each conversation goes on **one line**.

**An in-app chat from your own product.** A bot hands off to an agent. It includes an internal note (`public: false`) and per-message `attributes`.

```json theme={null}
{
  "conversation": {
    "conversation_id": "app_chat_20931",
    "started_at": "2026-09-28T09:15:00Z",
    "ended_at": "2026-09-28T09:31:42Z",
    "searchable": {"channel": "in_app", "app_version": "4.12.0", "plan": "enterprise", "account_id": "acct_771", "resolved": true}
  },
  "events": [
    {"event_id": "1", "time": "2026-09-28T09:15:00Z", "author_role": "customer", "author_id": "user_5521", "author_name": "Dana Ruiz", "body": "Export to CSV keeps timing out on the reports page."},
    {"event_id": "2", "time": "2026-09-28T09:15:04Z", "author_role": "agent", "author_id": "bot", "author_name": "Support Bot", "body": "Thanks! Connecting you with a support specialist.", "attributes": {"is_bot": true}},
    {"event_id": "3", "time": "2026-09-28T09:18:30Z", "author_role": "agent", "author_email": "priya@yourco.com", "author_name": "Priya Shah", "body": "Hi! How many rows is the report you're exporting?"},
    {"event_id": "4", "time": "2026-09-28T09:20:11Z", "author_role": "customer", "author_id": "user_5521", "author_name": "Dana Ruiz", "body": "Around 400k."},
    {"event_id": "5", "time": "2026-09-28T09:22:00Z", "author_role": "agent", "author_email": "priya@yourco.com", "author_name": "Priya Shah", "body": "Known issue over 250k rows, linking ENG-4410.", "public": false},
    {"event_id": "6", "time": "2026-09-28T09:31:42Z", "author_role": "agent", "author_email": "priya@yourco.com", "author_name": "Priya Shah", "body": "For now, filter by month and export each one. A fix ships next week.", "attributes": {"macro": "large_export_workaround"}}
  ]
}
```

**An email ticket exported from a help desk.** Your ticket fields go in `searchable`. `ended_at` is left out, so the last message's time is used.

```json theme={null}
{
  "conversation": {
    "conversation_id": "ticket_88412",
    "started_at": "2026-09-25T16:40:00-04:00",
    "searchable": {"channel": "email", "priority": "high", "tags": "billing,refund", "csat": 2, "team": "billing_tier2"}
  },
  "events": [
    {"event_id": "msg_1", "time": "2026-09-25T16:40:00-04:00", "author_role": "customer", "author_email": "jordan@client.io", "author_name": "Jordan Park", "body": "I was charged twice for September. Please refund one of them."},
    {"event_id": "msg_2", "time": "2026-09-26T10:05:00-04:00", "author_role": "agent", "author_id": "agent_17", "author_name": "Chris Wu", "body": "I see both charges. I've refunded the duplicate, and it should show up in 5 to 7 days."},
    {"event_id": "msg_3", "time": "2026-09-26T11:30:00-04:00", "author_role": "customer", "author_email": "jordan@client.io", "author_name": "Jordan Park", "body": "5 to 7 days is too long. This is the second time this has happened."}
  ]
}
```

Values in `searchable` must be single values, not lists. Join a list like tags into one string, as in `"billing,refund"`.

## The API

### 1. Name the conversation source

```
POST /conversation_sources
```

```json theme={null}
{"name": "in-app chat", "description": "Chats from the help widget in our app."}
```

The response:

```json theme={null}
{"name": "in-app chat", "status": "created"}
```

Calling it again with the same `name` returns `status: "exists"`, so your script can always call it first.

To see your sources and their columns:

```
GET /conversation_sources
```

```json theme={null}
[
  {"name": "in-app chat", "description": "Chats from the help widget in our app.", "columns": ["channel", "plan", "csat"], "created_at": "2026-10-05T16:26:49Z", "table_url": "https://app.rippit.com/workspace/table/TBDNVN0N48I3S21J"},
  {"name": "sales calls", "description": "", "columns": null, "created_at": "2026-10-06T09:12:03Z", "table_url": null}
]
```

`columns` and `table_url` are `null` until the source's first upload. `table_url` opens the table in Rippit.

### 2. Ask for an upload slot

```
POST /conversation_blocks/bulk?source=in-app%20chat
```

`source` is the source's `name`, URL-encoded. Uploading to a name you haven't created returns `404`.

The response:

```json theme={null}
{
  "job_id": "6abc...",
  "upload_url": "https://rippit-bulk-ingest.s3.amazonaws.com/",
  "upload_fields": {"key": "...", "policy": "...", "...": "..."},
  "expires_at": "2026-09-30T15:00:00Z",
  "max_bytes": 134217728
}
```

The upload link expires after one hour.

### 3. Upload the file

Send a multipart form POST to `upload_url`. Include every entry in `upload_fields` exactly as you received it, and attach the file as `file`. Don't send your Rippit credentials on this request.

```python theme={null}
requests.post(up["upload_url"], data=up["upload_fields"], files={"file": open("chats.jsonl", "rb")})
```

A successful upload returns `204`. The file can be at most 128 MB, the value of `max_bytes`. Split bigger exports across several uploads.

### 4. Mark the upload finished

```
POST /jobs/{job_id}/start
```

This returns the job with `status: "queued"`.

### 5. Check the job

```
GET /jobs/{job_id}
```

Poll this every 10 to 30 seconds until `status` is `done` or `failed`.

```json theme={null}
{
  "job_id": "6abc...",
  "status": "done",
  "result": {
    "rows_received": 1000,
    "rows_written": 998,
    "rejected": [{"row": 17, "error": "events.0.author_role: Input should be 'agent' or 'customer'"}],
    "source": "in-app chat",
    "table_url": "https://app.rippit.com/workspace/table/TBDNVN0N48I3S21J"
  }
}
```

`row` is the 0-based line number in your file. Fix the rejected lines and upload just those again.

## Limits

| Limit | Default |
| - | - |
| File size | 128 MB per upload |
| Uploads | 10 per minute, 100 per hour, 100 per day |

Only uploads that were actually completed count toward the limits. If you hit one you get `429` with a `Retry-After` header. Wait that many seconds and try again.

## Errors

| Code | Meaning | Fix |
| - | - | - |
| `401` | The token is missing, wrong or expired. | Check the `apitoken` header, or create a new token. |
| `403` | Bulk upload isn't enabled for your account. | Ask your Rippit contact to turn it on. |
| `404` | The source or job wasn't found. | Create the source with `POST /conversation_sources` first, or check the `job_id`. You can only see your own account's sources and jobs. |
| `409` on `/start` | The file wasn't uploaded yet, or the job was already started. | Upload the file first, and call `/start` only once. |
| `413` | The file is over 128 MB. | Split it into smaller files. |
| `400 EntityTooLarge` from the upload link | The file is over 128 MB. | Split it into smaller files. |
| `429` | You hit the upload limit. | Wait for the `Retry-After` time. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.