# Ingest API

Bubbling is conversation analytics for AI chat. It reviews the content of every conversation, and it
reports what the words mean for the company.

A company connects its chatbot to Bubbling. From then on, every conversation passes through an
analysis pipeline that classifies it and records what happened inside it. Transcripts become a
dataset that a person can query and track over time.

Send your conversations to Bubbling from your own server, or from your chat widget in the visitor's
browser.

The host is `https://ingest.bubbling.ai`.

## 1. Endpoints

| Endpoint                 | Body                                       | Use                                                      |
| ------------------------ | ------------------------------------------ | -------------------------------------------------------- |
| `POST /v1/conversations` | `{"conversations": [...]}`, 1 to 100 items | A finished transcript, or a history import.              |
| `POST /v1/messages`      | one or more messages of one chat           | A live chat, where you do not hold the whole transcript. |

Both answer `{"accepted": <number>}`.

A batch is all or nothing. One invalid record makes the whole request a 400 that stores nothing.
If a request fails, send the same batch again.

## 2. Authentication

Send your API key as a bearer token:

```
Authorization: Bearer <your-api-key>
```

Create the key in your Bubbling dashboard. The key states which project the conversations belong to,
so no other header is needed.

The key writes and never reads. A page that holds it can add conversations to your project, and can
read no conversation of yours. You can therefore put the key in your chat widget. Anyone who copies
it out of your page can write conversations you did not send, so rotate the key in your dashboard if
that happens.

## 3. The conversation

```json
{
	"conversationId": "c_1043",
	"agentId": "support-bot",
	"channel": "chat",
	"startedAt": "2026-08-11T10:00:00Z",
	"visitor": {
		"visitorId": "u_88",
		"name": "Dana Levi",
		"email": "dana@example.com",
		"locale": "en-US",
		"traits": {"plan": "pro"}
	},
	"metadata": {"pageUrl": "https://example.com/pricing"},
	"ip": "203.0.113.7",
	"messages": [
		{
			"messageId": "m1",
			"role": "visitor",
			"content": "My export is empty.",
			"createdAt": "2026-08-11T10:00:00Z",
			"authorName": "Dana",
			"data": {}
		}
	]
}
```

| Field            | Required | Notes                                                                                                                                               |
| ---------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conversationId` | yes      | Your own id for the chat. Send it again to update the same conversation.                                                                            |
| `agentId`        | no       | Which bot or agent answered.                                                                                                                        |
| `channel`        | no       | One of `chat`, `email`, `ticket`, `voice`, `social`.                                                                                                |
| `isEnded`        | no       | Boolean. `true` lets monitors review without the quiet period. `false` reopens the conversation. |
| `startedAt`      | no       | ISO 8601. The default is the time of the earliest message.                                                                                          |
| `visitor`        | no       | All fields optional. `traits` must be an object.                                                                                                    |
| `metadata`       | no       | Any object of your own fields.                                                                                                                      |
| `ip`             | no       | IPv4 or IPv6. The default is the address the call arrives from. Bubbling resolves the city and the country only if you turn that on for the source. |
| `messages`       | yes      | At least one message.                                                                                                                               |

Message fields:

| Field        | Required | Notes                                                                                                                    |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `messageId`  | no       | Your own id for the message. The default is the position in the array.                                                   |
| `role`       | yes      | One of `visitor`, `bot`, `human_agent`, `system`, `unknown`. `user` becomes `visitor` and `assistant` becomes `unknown`. |
| `content`    | yes      | The text of the message.                                                                                                 |
| `createdAt`  | yes      | ISO 8601.                                                                                                                |
| `authorName` | no       | The display name of the writer.                                                                                          |
| `data`       | no       | Any object of your own fields.                                                                                           |

Send `bot` or `human_agent` if you know which one wrote the message. `assistant` becomes `unknown`,
because that word states only that the visitor did not write it.

Both endpoints accept `isEnded`. Without this field, new or changed messages clear a previous end
flag. An unchanged transcript preserves it. To mark a conversation as ended without a new message,
resend its last message with the same message ID and `isEnded: true`.

## 4. Messages as they happen

Send the turns of one chat as they are written. `messages` holds one or more of them:

```json
{
	"conversationId": "c_1043",
	"messages": [
		{"role": "visitor", "content": "My export is empty.", "createdAt": "2026-08-11T10:00:00Z"},
		{"role": "bot", "content": "I can help.", "createdAt": "2026-08-11T10:00:05Z"}
	]
}
```

A single `message` object is accepted in place of `messages`, and it means the same thing:

```json
{
	"conversationId": "c_1043",
	"message": {"role": "bot", "content": "I can help.", "createdAt": "2026-08-11T10:00:05Z"}
}
```

Send only the messages Bubbling does not have yet. The conversation keeps every message you sent
before, so a call adds to the transcript and never replaces it.

The optional `agentId`, `channel`, `visitor`, `metadata` and `ip` fields also apply here. There is no
`startedAt`: the earliest message states when the conversation started.

Send `messageId` if you have one. If you omit it, Bubbling derives the id from the role, the content
and the timestamp. A repeated call then updates the same message, and two identical messages at the
same second become one message.

## 5. Repeat writes

A conversation is identified by your `conversationId`, and a message by your `messageId`. So a
repeated write updates the same records and never duplicates them.

- A write adds and updates messages. It never deletes one.
- Bubbling orders the transcript by `createdAt`.
- To correct a message, send it again with the same `messageId` and the new content.

If you omit `messageId`, the position in the array is the id. In that case, send the whole transcript
every time. A later partial write overwrites the first messages of the conversation.

## 6. From the browser

`POST /v1/messages` and `POST /v1/conversations` accept a call from any web page. Both answer the
preflight, so a `fetch` from your chat widget needs no proxy of your own.

A call from the visitor's page carries the visitor's own address, so omit `ip` and let Bubbling read
it from the connection. A call from your server carries your datacenter's address instead, so send
`ip` yourself there.

## 7. Errors

| Status | Code                   | Cause                                                                                                                    |
| ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 400    | `conversation_invalid` | A field is missing or has the wrong type. The message names the field, for example `conversation.messages[2].createdAt`. |
| 400    | `batch_too_large`      | More than 100 conversations in one request.                                                                              |
| 401    | `ingest_unauthorized`  | The key is absent, malformed, or unknown.                                                                                |
