---
title: "REST API v1"
source: https://sumgenius.ai/docs/rest-api
generated: 2026-09-05
---

# REST API v1

Read and manage your ChatGenius data: contacts, conversations, messages, tags, appointments, and your team. One base URL, one API key, one envelope.

Webhook & API Add-on · $29/month · Requires Creator+ plan

Account

[*GET*/account](#get-account)

Contacts

[*GET*/contacts](#list-contacts)

[*GET*/contacts/{id}](#get-contact)

[*POST*/contacts](#create-contact)

[*PATCH*/contacts/{id}](#update-contact)

[*POST*/contacts/{id}/tags](#add-contact-tag)

[*DEL*/contacts/{id}/tags/{tag}](#remove-contact-tag)

[*DEL*/contacts/{id}](#delete-contact)

Custom fields

[*GET*/custom-fields](#list-custom-fields)

[*POST*/custom-fields](#create-custom-field)

[*PATCH*/custom-fields/{id}](#update-custom-field)

Conversations

[*GET*/conversations](#list-conversations)

[*GET*/conversations/{id}](#get-conversation)

[*PATCH*/conversations/{id}](#update-conversation)

[*POST*/conversations/{id}/assign](#assign-conversation)

[*DEL*/conversations/{id}](#delete-conversation)

Messages

[*GET*/conversations/{id}/messages](#list-messages)

[*POST*/messages](#send-message)

Tags

[*GET*/tags](#list-tags)

[*POST*/tags](#create-tag)

[*DEL*/tags/{id}](#delete-tag)

Appointments

[*GET*/appointments](#list-appointments)

[*GET*/appointments/{id}](#get-appointment)

Flows

[*GET*/flows](#list-flows)

[*GET*/flows/{id}](#get-flow)

Flow sessions

[*GET*/flow-sessions](#list-flow-sessions)

[*GET*/flow-sessions/{id}](#get-flow-session)

[*GET*/flow-sessions/{id}/events](#list-flow-session-events)

[*GET*/flow-events](#list-flow-events)

Opt-outs

[*GET*/opt-outs](#list-opt-outs)

Team

[*GET*/team](#get-team)

AI configuration

[*GET*/ai/config](#get-ai-config)

[*PATCH*/ai/config](#update-ai-config)

[*GET*/ai/channels](#get-ai-channels)

[*PATCH*/ai/channels/{channel}](#update-ai-channel)

Knowledge base

[*GET*/knowledge](#knowledge-usage)

[*GET*/knowledge/faqs](#knowledge-faqs)

[*POST*/knowledge/faqs](#knowledge-faqs)

[*PATCH*/knowledge/faqs/{id}](#knowledge-faqs)

[*DEL*/knowledge/faqs/{id}](#knowledge-faqs)

[*GET*/knowledge/services](#knowledge-services)

[*POST*/knowledge/services](#knowledge-services)

[*PATCH*/knowledge/services/{id}](#knowledge-services)

[*DEL*/knowledge/services/{id}](#knowledge-services)

[*GET*/knowledge/documents](#knowledge-documents)

[*PATCH*/knowledge/documents/{id}](#knowledge-documents)

[*DEL*/knowledge/documents/{id}](#knowledge-documents)

[*GET*/knowledge-gaps](#knowledge-gaps)

Media library

[*GET*/media](#media)

[*POST*/media](#media)

[*PATCH*/media/{id}](#media)

[*DEL*/media/{id}](#media)

Blocked users

[*GET*/blocked-users](#blocked-users)

[*POST*/blocked-users](#blocked-users)

[*DEL*/blocked-users/{id}](#blocked-users)

Comment triggers

[*GET*/comment-triggers](#comment-triggers)

[*POST*/comment-triggers](#comment-triggers)

[*GET*/comment-triggers/{id}](#comment-triggers)

[*PATCH*/comment-triggers/{id}](#comment-triggers)

[*DEL*/comment-triggers/{id}](#comment-triggers)

[*POST*/comment-triggers/{id}/duplicate](#comment-triggers)

[*GET*/comment-triggers/{id}/stats](#comment-triggers)

[*GET*/comment-triggers/simulate](#simulate-comment)

[*GET*/posts](#list-posts)

Comments (AI Comment Manager)

[*GET*/comments](#comments)

[*GET*/comments/{id}](#comments)

[*GET*/comments/stats](#comments)

[*POST*/comments/{id}/approve](#comments)

[*POST*/comments/{id}/reject](#comments)

[*POST*/comments/approve](#comments)

[*POST*/comments/reject](#comments)

### Machine-readable spec

Everything on this page, as an OpenAPI 3.1 document: [https://sumgenius.ai/api/v1/openapi.json](https://sumgenius.ai/api/v1/openapi.json). No key needed. Every endpoint, parameter, field, and allowed value is in it, including the scope each endpoint needs and the full list of webhook event names.

Give that URL to an SDK generator, an API client, or an AI assistant and it has the complete contract. The enums in it are generated from the same constants the API validates against, so they cannot drift from what the API accepts.

This page is also available as Markdown at [/docs/rest-api.md](https://sumgenius.ai/docs/rest-api.md).

**Fetch the spec**

```
curl https://sumgenius.ai/api/v1/openapi.json
```

### Authentication

Every request is authenticated with your API key from the portal's *Webhook & Send API* page. The same key opens the REST API, the [Send API](https://sumgenius.ai/docs/send-api), and [webhooks](https://sumgenius.ai/docs/chatgenius-webhook-api). Send it as any one of three headers; `X-SumGenius-Api-Key` is preferred.

The API is server-to-server. CORS is intentionally disabled, so call it from your backend and keep the key out of browsers.

- 401 unauthorized No API key sent.
- 401 invalid_api_key Key not recognized.
- 403 addon_inactive Key is valid but the add-on is not active.

**Base URL**

```
https://sumgenius.ai/api/v1
```

**Authenticated request**

```
curl https://sumgenius.ai/api/v1/account \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

### Envelope, errors & pagination

The HTTP status carries success or failure. A single resource arrives as `{"data": {...}}`; a list adds `has_more` and `next_cursor`. Errors carry a stable `code` to branch on; `message` is human-facing and may change.

Lists are newest-first and cursor-paginated: pass `limit` (1 to 100, default 25) and feed `next_cursor` back as `starting_after` until `has_more` is `false`. Tags and team return their whole set in one page.

| HTTP | code | When |
| --- | --- | --- |
| 400 | invalid_param | A parameter failed validation; `param` names it |
| 400 | invalid_json | Body is not valid JSON |
| 403 | insufficient_scope | The key's [scope](https://sumgenius.ai/docs/api-authentication#scopes) does not cover this endpoint |
| 403 | plan_limit_reached | The plan cap for this resource is reached |
| 403 | feature_unavailable | The plan does not include this feature; `param` names it |
| 404 | not_found | Missing, or not owned by your account |
| 409 | resume_blocked · blocked · opted_out · window_closed | A conversation pause or resume the portal rules refuse; the message says why |
| 409 | already_blocked | That person is already on the AI blocklist |
| 409 | purge_active · sms_not_ready | SMS AI cannot be switched on: inbound body purge is on, or no Messaging Service yet |
| 409 | flow_conflict | A comment trigger cannot be activated because of its linked flows: more than one, a paused one, or a broken link; the message says which |
| 409 | not_connected | The channel you asked about is not connected to this account |
| 409 | idempotency_in_progress | A request with this `Idempotency-Key` is still running; retry after `Retry-After` |
| 400 | invalid_idempotency_key | `Idempotency-Key` is not 1 to 128 printable ASCII characters |
| 405 | method_not_allowed | Wrong method; the `Allow` header lists the valid ones |
| 422 | idempotency_key_reused | Same `Idempotency-Key`, different request |
| 422 | unprocessable_send | A send was permanently rejected by the channel: unsupported action, expired window, invalid recipient |
| 405 |  | Wrong method; the `Allow` header lists valid ones |
| 429 | rate_limited | REST request quota or burst ceiling reached |
| 503 | service_unavailable | Temporary safety or infrastructure failure; retry as directed |
| 500 | server_error | Unexpected failure on our side |

**List envelope**

```
{
  "data": [ ... ],
  "has_more": true,
  "next_cursor": "18son403"
}
```

**Error envelope**

```
{
  "error": {
    "code": "invalid_param",
    "message": "stage must be one of: new, contacted, qualified, customer, lost.",
    "param": "stage"
  }
}
```

### Idempotency

Send an `Idempotency-Key` header on any `POST`, `PATCH` or `DELETE` and a retry of the same request returns the same response instead of running twice. Use it whenever a network failure would leave you unsure whether the first attempt landed: creating records, sending messages, and later, bookings and posts.

- Idempotency-Keyheader, 1 to 128 printable characters Any string unique to the operation on your side, for example your own order or job id, or a UUID. Keys are scoped to your account and remembered for 24 hours.
- Replay A retry with the same key and the same method, path and body gets the original status and body back, with the header `Idempotent-Replayed: true`. Both successes and 4xx errors are replayed. A 5xx is never stored, so the same key can be retried after a server fault.
- Conflict The same key with a different body, path or method is `422 idempotency_key_reused`. A retry that arrives while the first request is still running is `409 idempotency_in_progress` with `Retry-After`.

[POST /messages](#send-message) also accepts `idempotency_key` in the body, which the send engine deduplicates on its own. The header works there too, and using both is harmless.

**Request**

```
curl -X POST https://sumgenius.ai/api/v1/contacts \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Idempotency-Key: crm-sync-8842" \
  -H "Content-Type: application/json" \
  -d '{"channel":"instagram","platform_user_id":"17845629031224067","name":"Riley"}'
```

**Retry (same key, same body)**

```
HTTP/1.1 201 Created
Idempotent-Replayed: true
Content-Type: application/json

{ "data": { ...the original response... } }
```

### Rate limits

Each account may make up to **200 authenticated REST requests per minute**, with short bursts of up to **20 requests per second**. All active API keys for the account share the same quota, so creating another key does not increase it.

The per-minute quota is measured over a rolling 60-second window rather than a fixed clock minute, so capacity returns gradually as individual requests age out instead of all at once on a boundary.

A separate system-wide safety ceiling protects API availability during aggregate traffic spikes. When either ceiling is reached, the API returns `429 rate_limited`. Wait for `Retry-After` before retrying; use exponential backoff rather than an immediate retry loop.

Every authenticated REST response includes `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`. Reset is a Unix timestamp. The equivalent `X-RateLimit-*` headers are also sent for compatibility.

This quota controls requests accepted by `/api/v1`. Message platforms enforce their own separate, account-specific delivery limits; temporary platform throttles continue through the existing queue and retry behavior.

**Rate-limit headers**

```
RateLimit-Limit: 200
RateLimit-Remaining: 182
RateLimit-Reset: 1785456060
```

**Response 429**

```
Retry-After: 1

{
  "error": {
    "code": "rate_limited",
    "message": "Too many REST API requests. Retry after the number of seconds in Retry-After."
  }
}
```

### Data conventions

- timestampsUTC ISO-8601 Everywhere, e.g. `2026-07-25T04:35:23+00:00`. The `updated_since` filters accept the same format.
- channelenum `facebook` · `instagram` · `sms` · `telegram` · `whatsapp`
- stageenum `new` · `contacted` · `qualified` · `customer` · `lost`
- statusenum `active` · `resolved` · `pending` · `archived` · `escalated` (conversations)
- custom_fieldsobject Your own per-contact data, as `{field_key: value}`. Only fields that are set and active appear; unset fields are omitted, never `null`. Define fields with [POST /custom-fields](#create-custom-field), write values through [PATCH /contacts/{id}](#update-contact).

**Identity**

```
// A contact is keyed by its natural identity:
(channel, platform_user_id)

// e.g. an Instagram user:
("instagram", "17845629031224067")
```

## Account

### GET /account

Your account identity, plan, and configured webhook endpoints. A good first call to verify a key works.

**Request**

```
curl https://sumgenius.ai/api/v1/account \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200**

```
{
  "data": {
    "account": {
      "id": 1042,
      "business_name": "Riverside Dental",
      "plan": "Professional",
      "addon": "webhook_api"
    },
    "api_key": { "id": 6, "scope": "write" },
    "webhooks": {
      "endpoints": [
        {
          "id": 7,
          "name": "Production endpoint",
          "url": "https://api.example.com/webhooks",
          "enabled": true,
          "event_filters": ["message.received"],
          "envelope_style": "standard"
        }
      ]
    }
  }
}
```

## Contacts

A contact is a person, keyed by `(channel, platform_user_id)`. Contacts are created automatically when someone messages you, or explicitly via the API.

### GET /contacts

List contacts, newest first. Filters combine with AND. An invalid filter value returns `400 invalid_param`.

Query parameters

- channelenumoptional
- stageenumoptional
- tagstringoptional Exact tag name; contacts carrying that tag.
- emailstringoptional Exact match.
- phonestringoptional Exact match.
- namestringoptional Substring match on the display name.
- updated_sincetimestampoptional Contacts updated at or after this time.
- limit · starting_afterpaginationoptional

**Request**

```
curl "https://sumgenius.ai/api/v1/contacts?stage=qualified&limit=25" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · the contact resource**

```
{
  "data": [
    {
      "id": 19214,
      "channel": "instagram",
      "platform_user_id": "17845629031224067",
      "name": "Riley Chen",
      "email": "riley@example.com",
      "phone": null,
      "ig_username": "rileychen",
      "profile_pic_url": null,
      "stage": "qualified",
      "tags": ["VIP"],
      "lead_score": 30,
      "lead_temperature": "warm",
      "notes": null,
      "custom_fields": { "crm_deal_id": "D-2214" },
      "first_seen_at": "2026-07-25T04:35:23+00:00",
      "first_user_message_at": "2026-07-25T04:36:10+00:00",
      "last_seen_at": "2026-07-26T08:05:04+00:00",
      "created_at": "2026-07-25T04:35:23+00:00",
      "updated_at": "2026-07-26T08:05:04+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### GET /contacts/{id}

One contact by id, in the same shape as the list. A non-numeric, unknown, or unowned id returns `404`.

**Request**

```
curl https://sumgenius.ai/api/v1/contacts/19214 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

### POST /contacts

Create or update a contact by its natural identity. A new identity returns `201`; an existing one is updated and returns `200`. Updates are partial: omitted fields are untouched and existing tags are kept (`tags` adds, it does not replace).

Body

- channelenumrequired
- platform_user_idstringrequired The person's id on that channel, 1 to 255 characters. For SMS this is the phone number.
- namestringoptional Up to 255 characters.
- emailstringoptional Valid email address.
- phonestringoptional Digits and `+ ( ) . - # x`, 3 to 50 characters.
- stageenumoptional
- notesstringoptional Up to 5,000 characters.
- tagsstring\[\]optional Tag names; missing tags are created automatically.
- custom_fieldsobjectoptional Merged into the contact's fields, applied atomically with the rest of the upsert. Same semantics as [PATCH](#update-contact). Fields must already be defined.

**Request**

```
curl -X POST https://sumgenius.ai/api/v1/contacts \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "platform_user_id": "+17025550100",
    "name": "Riley Chen",
    "stage": "new",
    "tags": ["Imported"]
  }'
```

**Response 201 · 200 on update**

```
{
  "data": {
    "id": 19301,
    "channel": "sms",
    "platform_user_id": "+17025550100",
    "name": "Riley Chen",
    "stage": "new",
    "tags": ["Imported"],
    ...
  }
}
```

### PATCH /contacts/{id}

Partial update by id. Only fields present in the body change. Send `""` to clear a field. Idempotent by nature, so no idempotency key is needed. An empty body returns `400` listing the updatable fields.

Body · all optional

- stageenum
- namestring Up to 255 characters, or `""` to clear.
- emailstring Valid email, or `""` to clear.
- phonestring 3 to 50 characters, or `""` to clear.
- notesstring Up to 5,000 characters, or `""` to clear.
- custom_fieldsobject Merge semantics: only the keys you send change, `null` as a value clears that field, other fields are untouched. Values are validated against each field's type (`400` names the offending key). Every actual change fires a [contact.field_changed](https://sumgenius.ai/docs/webhook-events#contact-field-changed) webhook event.

**Request**

```
curl -X PATCH https://sumgenius.ai/api/v1/contacts/19214 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "stage": "customer",
    "notes": "Paid annual plan on Jul 29"
  }'
```

**Response 200**

```
{
  "data": {
    "id": 19214,
    "stage": "customer",
    "notes": "Paid annual plan on Jul 29",
    ...
  }
}
```

### POST /contacts/{id}/tags

Attach a tag by name. A tag that does not exist yet is created, then attached. Re-attaching is a no-op `200`. Returns the updated contact.

Body

- namestringrequired 1 to 50 characters. Created if missing, with the default color.

**Request**

```
curl -X POST https://sumgenius.ai/api/v1/contacts/19214/tags \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "VIP"}'
```

### DELETE /contacts/{id}/tags/{tag}

Detach a tag from a contact. `{tag}` is a tag id or a URL-encoded name; a name that is all digits or contains `/` must go by id. Detaching a tag the contact does not have is an idempotent `200`; a tag that does not exist at all is `404`. Only the attachment is removed; the tag itself survives.

**Request**

```
curl -X DELETE https://sumgenius.ai/api/v1/contacts/19214/tags/VIP \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

### DELETE /contacts/{id}

Permanently erases a person: the contact and every conversation, message, and appointment they have. Built for right-to-erasure requests.

> **Warning**
>
> **This cannot be undone.** To remove one thread and keep the person, use [delete conversation](#delete-conversation) instead.

Tag definitions and send-history audit records survive. Repeating the call is a safe `404`.

**Request**

```
curl -X DELETE https://sumgenius.ai/api/v1/contacts/19214 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200**

```
{
  "data": {
    "id": 19214,
    "object": "contact",
    "deleted": true
  }
}
```

## Custom fields

Your own per-contact data. Define up to 15 fields (text, number, date, or boolean), then write values through [PATCH /contacts/{id}](#update-contact). Values appear in every contact resource and in full-profile webhook payloads, and every change fires a [contact.field_changed](https://sumgenius.ai/docs/webhook-events#contact-field-changed) event. Don't store passwords, API keys, or payment card numbers in field values.

### GET /custom-fields

Your field definitions, in creation order. Active fields only by default.

Query parameters

- include_archivedbooleanoptional Also return archived definitions (with `archived` true) so their ids stay discoverable for unarchiving.

**Request**

```
curl https://sumgenius.ai/api/v1/custom-fields \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200**

```
{
  "data": [
    {
      "id": 3,
      "object": "custom_field",
      "field_key": "crm_deal_id",
      "label": "CRM deal ID",
      "type": "text",
      "archived": false,
      "created_at": "2026-08-01T18:20:11+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### POST /custom-fields

Define a field. Returns `201` on create, `200` if an equivalent field already exists (same key and type), `409` if the key exists with a different type or archived, and `400` at the 15-active-field limit.

Body

- field_keystringrequired 1 to 50 characters of `a-z 0-9 _`. **Permanent after create**, it is the JSON key in every payload.
- labelstringrequired Display name, 1 to 100 characters. Renameable later.
- typeenumoptional `text` (default, up to 500 chars) · `number` · `date` (`YYYY-MM-DD`) · `boolean`. Permanent after create.

**Request**

```
curl -X POST https://sumgenius.ai/api/v1/custom-fields \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "field_key": "crm_deal_id",
    "label": "CRM deal ID",
    "type": "text"
  }'
```

**Then set a value**

```
curl -X PATCH https://sumgenius.ai/api/v1/contacts/19214 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "custom_fields": { "crm_deal_id": "D-2214" }
  }'
```

### PATCH /custom-fields/{id}

Rename a field's label, or archive / unarchive it. `field_key` and `type` are immutable, sending either returns `400`. There is no delete: archiving hides a field from every surface without touching any contact data, and unarchiving restores it (subject to the 15-active limit).

Body

- labelstringoptional
- archivedbooleanoptional `true` hides the field everywhere; `false` restores it, values intact. Writes to an archived field's values return `400`.

**Request · archive**

```
curl -X PATCH https://sumgenius.ai/api/v1/custom-fields/3 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "archived": true }'
```

## Conversations

One thread per person per channel. Deleted conversations are never returned.

### GET /conversations

List conversations, newest first. `contact_id` links each thread to its person.

Query parameters

- channelenumoptional
- statusenumoptional
- assigned_tointegeroptional A team member id from [team](#get-team).
- updated_sincetimestampoptional
- limit · starting_afterpaginationoptional

> **Note**
>
> **`assigned_to` id space:** positive is a team member (`assigned_to_name` resolves); negative is an owner-level assignment (`assigned_to_name` is `null`); `null` is unassigned.

**Request**

```
curl "https://sumgenius.ai/api/v1/conversations?status=escalated" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · the conversation resource**

```
{
  "data": [
    {
      "id": 188892,
      "channel": "instagram",
      "platform_user_id": "17845629031224067",
      "customer_name": "Riley",
      "status": "escalated",
      "assigned_to": 90011,
      "assigned_to_name": "Sam Ortiz",
      "ai_paused": true,
      "ai_pause_reason": "assigned",
      "contact_id": 19214,
      "lead_email": "riley@example.com",
      "lead_phone": null,
      "last_message_at": "2026-07-26T08:05:04+00:00",
      "created_at": "2026-07-25T04:35:23+00:00",
      "updated_at": "2026-07-26T13:00:54+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### GET /conversations/{id}

One conversation by id, in the same shape as the list. A non-numeric, unknown, or unowned id returns `404`.

**Request**

```
curl https://sumgenius.ai/api/v1/conversations/188892 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

### PATCH /conversations/{id}

Change a conversation's status, or take it away from the AI and hand it back. Two optional fields, send one or both. Returns the conversation, which now carries `ai_paused` and `ai_pause_reason` so you can see the effect. Needs the `write` scope.

- statusstringoptional `active`, `resolved` or `archived`. Resolving or archiving hands the thread back to the AI, exactly like the inbox. `pending` and `escalated` are set by the system and cannot be sent.
- ai_pausedbooleanoptional `true` takes the thread away from the AI as the account owner, the inbox Claim button. It reopens a resolved thread. Refused with `409` when the customer is blocked, has opted out, or the Meta messaging window has closed. `false` hands the thread back to the AI, the inbox Transfer to AI. Refused with `409 resume_blocked` when the pause came from a solicitation pause, a fraud flag or a connected-account pause; those are lifted in the portal only.
- ai_pause_reasonin the response `escalated`, `assigned`, `owner_claimed`, `solicitation`, `fraud`, `connected_account`, or `null` when the AI is live. The AI is muted on any thread that is assigned or escalated; this field tells you which.

Status is applied first, then `ai_paused`. Fires [conversation.resolved](https://sumgenius.ai/docs/webhook-events#conversation-resolved) and [conversation.assigned](https://sumgenius.ai/docs/webhook-events#conversation-assigned) as appropriate.

**Pause the AI on one thread**

```
curl -X PATCH https://sumgenius.ai/api/v1/conversations/188892 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"ai_paused": true}'
```

**Resolve it**

```
curl -X PATCH https://sumgenius.ai/api/v1/conversations/188892 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"status": "resolved"}'
```

**409 resume_blocked**

```
{
  "error": {
    "code": "resume_blocked",
    "message": "The AI is paused on this conversation by a fraud flag. That can only be lifted from the portal.",
    "param": "ai_paused"
  }
}
```

### POST /conversations/{id}/assign

Assign or unassign a conversation, exactly like the inbox does it: a `pending` or `escalated` thread becomes `active`, history is recorded, and a system line is added to the transcript. Returns the updated conversation.

Body

- assigned_tointeger | nullrequired A team member id from [team](#get-team), or `null` to unassign. An unknown id is `400`.

**Request**

```
curl -X POST https://sumgenius.ai/api/v1/conversations/188892/assign \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"assigned_to": 90011}'
```

### DELETE /conversations/{id}

Permanently deletes one conversation: the full transcript, in-chat appointments, tag labels, and assignment records.

> **Warning**
>
> **This cannot be undone.** The contact survives; to erase the person entirely use [delete contact](#delete-contact).

The response counts what was removed. Analytics rows are kept but stripped of message text and unlinked; send-history audit survives. A repeat delete is a safe `404`.

**Request**

```
curl -X DELETE https://sumgenius.ai/api/v1/conversations/188892 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200**

```
{
  "data": {
    "id": 188892,
    "object": "conversation",
    "deleted": true,
    "removed": {
      "messages": 15,
      "appointments": 1,
      "conversation_tags": 2,
      "contexts": 1,
      "metrics_detached": 0
    }
  }
}
```

## Messages

### GET /conversations/{id}/messages

The transcript of one conversation, newest first (default limit 50). Copilot drafts are excluded; this is the conversation as the customer saw it.

- directionderived `user` is inbound; `bot`, `agent`, `followup` are outbound; the rest is system.
- typeenum `text` · `image` · `video` · `audio` · `file` · `quick_reply` · `button` · `postback` · `comment_trigger`
- body_purgedboolean When a body was purged for compliance, `text` is `null` and this is `true`; the message is still listed.

**Request**

```
curl "https://sumgenius.ai/api/v1/conversations/188892/messages" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · the message resource**

```
{
  "data": [
    {
      "id": 56698,
      "message_id": "usr_6a65bfb099523",
      "direction": "inbound",
      "sender_type": "user",
      "sender_name": null,
      "type": "text",
      "text": "Do you have anything open Friday?",
      "attachments": [],
      "is_template": false,
      "body_purged": false,
      "created_at": "2026-07-26T08:05:04+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "56610"
}
```

### POST /messages

Send on any channel. Same engine as the [Send API](https://sumgenius.ai/docs/send-api) with full parity; recommended for new integrations. Route by `conversation_id`, by `platform` + `platform_user_id`, or by `to_phone` for SMS and WhatsApp. To reply privately to an Instagram or Facebook comment (with optional buttons), add `comment_id`, full reference under [private reply](https://sumgenius.ai/docs/send-api#private-reply); the body is identical here.

Body

- idempotency_keystringrequired Your unique key per send. A replay returns the original result without resending. Also accepted as an `Idempotency-Key` header.
- actionenumoptional `send_message` (default) · `react` · `unreact` · `typing_on`. Support varies by channel.
- conversation_idinteger Route by an existing thread.
- platform + platform_user_idstring Route by the person's channel id.
- to_phonestring Route by phone (SMS, WhatsApp).
- message_textstringrequired Not required for `typing_on` or WhatsApp template sends (`template_name`, `template_language`, `template_components`).

Cold outbound

SMS, WhatsApp, and Telegram may send to a recipient with no existing conversation; the thread is created automatically. Facebook and Instagram require the customer to message first.

> **Warning**
>
> A failed cold send still creates the conversation and contact before the delivery attempt. Repeats reuse the same records; remove them with [delete contact](#delete-contact) if unwanted.

Send errors

| HTTP | code | When |
| --- | --- | --- |
| 400 | invalid_request | Missing or invalid field |
| 403 | forbidden | Channel not permitted |
| 404 | not_found | No conversation for the routing given |
| 410 | recipient_opted_out | Recipient opted out (SMS STOP) |
| 412 | not_provisioned | Channel not connected |
| 422 | unprocessable_send | Permanent rejection: unsupported action, expired window, invalid recipient |
| 429 | rate_limited | Too many sends; retry later |
| 503 | service_unavailable | Temporary failure; may queue for retry |

**Request**

```
curl -X POST https://sumgenius.ai/api/v1/messages \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "order-4821-confirm",
    "conversation_id": 188892,
    "message_text": "Your order shipped today."
  }'
```

**Response 200**

```
{
  "data": {
    "status": "sent",
    "request_id": "msgreq_c8d83cf861ec1f48",
    "conversation_id": 188892,
    "platform": "instagram",
    "action": "send_message",
    "sent_at": "2026-07-30T06:14:18+00:00",
    "message": "Message sent successfully."
  }
}
```

**Error 422**

```
{
  "error": {
    "code": "unprocessable_send",
    "message": "action 'react' is not supported for platform 'telegram'.",
    "status": "dead_letter"
  }
}
```

## Tags

Tag definitions live account-wide; attaching them to contacts is a [contact operation](#add-contact-tag).

### GET /tags

All tag definitions, ordered by name, in one page. `name` is what the contacts `?tag=` filter matches.

**Request**

```
curl https://sumgenius.ai/api/v1/tags \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200**

```
{
  "data": [
    {
      "id": 14,
      "name": "Booking",
      "color": "#DD6B20",
      "created_at": "2026-03-06T19:55:17+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### POST /tags

Create a tag definition. Idempotent on name: new returns `201`, existing returns `200` with its color unchanged.

Body

- namestringrequired 1 to 50 characters, unique per account.
- colorstringoptional 6-digit hex like `#FF0000`; anything else is `400`.

**Request**

```
curl -X POST https://sumgenius.ai/api/v1/tags \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "VIP", "color": "#FF0000"}'
```

### DELETE /tags/{id}

Deletes a tag definition account-wide and removes it from every contact and conversation carrying it. By numeric id only.

**Request**

```
curl -X DELETE https://sumgenius.ai/api/v1/tags/14 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200**

```
{"data": {"id": 14, "deleted": true}}
```

## Appointments

Bookings made through the [AI booking agent](https://sumgenius.ai/docs/appointment-booking). Read-only.

### GET /appointments

List bookings, newest first.

Query parameters

- statusstringoptional Exact match, e.g. `confirmed` or `cancelled`. Free text: an unknown value returns an empty list, not `400`.
- conversation_idintegeroptional
- limit · starting_afterpaginationoptional

> **Note**
>
> **`datetime` is UTC, booked in `timezone`.** Convert back with the `timezone` field to display the time the customer chose. `meeting_link` and `google_event_id` are `null` without Google Calendar sync.

**Request**

```
curl "https://sumgenius.ai/api/v1/appointments?status=confirmed" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · the appointment resource**

```
{
  "data": [
    {
      "id": 91772,
      "confirmation_code": "SG-FHZS9V",
      "conversation_id": 187864,
      "contact_id": 16240,
      "channel": "instagram",
      "datetime": "2026-07-24T23:00:00+00:00",
      "timezone": "America/Los_Angeles",
      "duration_minutes": 60,
      "service_type": "Consultation",
      "cost": "0.00",
      "status": "confirmed",
      "meeting_link": "https://meet.google.com/abc-defg-hij",
      "google_event_id": "oa9daqujesg8dktkb777ds5gs0",
      "reminder_sent": false,
      "location": {"city": null, "state": null},
      "customer": {
        "name": "Riley",
        "email": "riley@example.com",
        "phone": "+17025550100",
        "address": null
      },
      "notes": "Prefers mornings",
      "created_at": "2026-07-24T07:33:22+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### GET /appointments/{id}

One appointment by id, same shape as the list. Unknown or unowned id returns `404`.

**Request**

```
curl https://sumgenius.ai/api/v1/appointments/91772 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

> **Note**
>
> **Recording consent.** On Instagram and Messenger the basis for replying is that the person messaged you first, and `first_user_message_at` on the contact is when that happened. It is `null` for someone who only ever commented and never opened a DM. If your flow collects an explicit consent answer, the value lives in `custom_fields`, and the [contact.field_changed](https://sumgenius.ai/docs/webhook-events) webhook delivers the `changed_at` and `source` alongside it at the moment it is set. Reading the contact back later returns the current value only, so keep the webhook if you need the audit trail.

## Flows

Your automations, as built in the [flow builder](https://sumgenius.ai/docs/chatgenius-flow-builder). Read-only. This is the parent of [flow sessions](#list-flow-sessions): a session records the `flow_id` and `flow_version` it ran under, and this endpoint is what makes those numbers mean something.

### GET /flows

List flows, newest first.

Query parameters

- statusstringoptional `draft`, `active` or `paused`.
- trigger_typestringoptional `keyword`, `first_message`, `comment_trigger` or `button_tap`.
- limit · starting_afterpaginationoptional

> **Note**
>
> **Flow definitions are not returned here.** The canvas itself, its nodes and its wiring, exports as JSON from the builder. What starts the flow is: `trigger.sources[]` lists each source (`dm`, `comment`, `ai_intent`, `story_reply`...) with its keywords; a `comment` source with `mode: linked` and a `comment_trigger_id` belongs to a [comment trigger](#comment-triggers), while `mode: native` means the flow matches its own keywords. This endpoint gives you what you need to read session and event data. `completion_rate` is `null` when nobody has entered the flow, which is not the same as 0%.

**Request**

```
curl "https://sumgenius.ai/api/v1/flows?status=active" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · the flow resource**

```
{
  "data": [
    {
      "id": 178,
      "name": "Comment to guide",
      "status": "active",
      "trigger_type": "comment_trigger",
      "channels": ["instagram"],
      "version": 4,
      "published_version": 4,
      "priority": 0,
      "stats": {
        "entries": 312,
        "completions": 188,
        "completion_rate": 0.6026
      },
      "created_at": "2026-08-02T14:10:03+00:00",
      "updated_at": "2026-08-26T15:41:55+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### GET /flows/{id}

Retrieve one flow. Returns the same resource as the list. `404` if it does not exist, was deleted, or belongs to another account.

**Request**

```
curl https://sumgenius.ai/api/v1/flows/178 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

## Flow sessions

One session per person per run of a flow, plus the ordered path they took through it. This is how an automation performed: which version ran, which nodes were reached, which branch was taken, where someone stopped, and which post or keyword brought them in.

### GET /flow-sessions

List sessions, newest first.

Query parameters

- flow_idintegeroptional
- statusstringoptional `waiting_input`, `delayed`, `completed`, `expired` or `cancelled`.
- channelstringoptional `instagram`, `facebook` or `whatsapp`.
- contact_idintegeroptional Every run by one person. An unknown contact returns `404` rather than an empty list, so a bad id cannot look like "no activity".
- updated_sinceISO-8601optional Sessions touched since this time. Use it to poll incrementally.
- limit · starting_afterpaginationoptional

> **Note**
>
> **Structure, never content.** Values a flow collected, and anything a person typed, are not returned by this endpoint. `attribution` carries only the originating post, comment and matched keyword. Those are the same fields that survive the 90-day retention sweep, so a session you fetch today and the same session a year from now have the same shape.

**Request**

```
curl "https://sumgenius.ai/api/v1/flow-sessions?flow_id=178&status=completed" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · the flow session resource**

```
{
  "data": [
    {
      "id": 6075,
      "flow_id": 178,
      "flow_name": "Comment to guide",
      "flow_version": 1,
      "channel": "instagram",
      "contact_id": 21877,
      "status": "completed",
      "end_reason": null,
      "current_node_id": "9",
      "attribution": {
        "post_id": "18378452308239549",
        "comment_id": "18001122334455667",
        "matched_keyword": "GUIDE"
      },
      "started_at": "2026-08-26T15:47:23+00:00",
      "last_activity_at": "2026-08-26T15:47:31+00:00",
      "last_user_message_at": null,
      "delay_until": null,
      "window_expires_at": "2026-08-27T15:47:23+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "6074"
}
```

### GET /flow-sessions/{id}

Retrieve one session. Same resource as the list.

**Request**

```
curl https://sumgenius.ai/api/v1/flow-sessions/6075 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

### GET /flow-sessions/{id}/events

The ordered path through the flow, oldest first. Every step a person reached and every branch they took.

Query parameters

- typestringoptional One event type: `session_started`, `node_entered`, `node_exited`, `branch_selected`, `input_received`, `link_clicked`, `outcome_achieved`, `session_completed`, `session_expired`, `session_cancelled`, `session_reactivated`.
- limit · starting_afterpaginationoptional Default `100` here, and ordered ascending so pages read in sequence.

> **Note**
>
> **No message text is stored on an event.** An `input_received` event records that input arrived and what kind it was, never what was said. That is why this data can outlive the conversation it came from.

**Request**

```
curl "https://sumgenius.ai/api/v1/flow-sessions/6075/events" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · the event resource**

```
{
  "data": [
    {
      "id": 7813,
      "session_id": 6075,
      "flow_id": 178,
      "flow_version": 1,
      "type": "branch_selected",
      "node_key": "1",
      "node_type": "trigger",
      "output_port": "output_1",
      "input_type": "system",
      "end_reason": null,
      "occurred_at": "2026-08-26T15:47:23+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### GET /flow-events

The same events, flat across every session. Use this one to analyse how a flow performs; use the nested route above when you want one person's path.

Query parameters

- flow_idintegeroptional
- flow_sessionintegeroptional One session's events, in the flat shape. Named `flow_session` rather than `session_id` because web application firewalls commonly block that parameter name outright. The field in the response is still `session_id`.
- typestringoptional Same event types as the nested route.
- node_keystringoptional One step of the flow. Combine with `type=branch_selected` to see every choice made at a single decision point.
- sinceISO-8601optional
- limit · starting_afterpaginationoptional

> **Note**
>
> **Built for incremental syncing.** Events are ordered by id ascending and ids only ever increase, so you can store the last `next_cursor` and keep paging forward without ever re-reading a row. Pulling a whole flow's history is one loop, not one call per session.

**Request**

```
curl "https://sumgenius.ai/api/v1/flow-events?flow_id=178&type=branch_selected" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · same event resource**

```
{
  "data": [
    {
      "id": 7813,
      "session_id": 6075,
      "flow_id": 178,
      "flow_version": 1,
      "type": "branch_selected",
      "node_key": "4",
      "node_type": "question",
      "output_port": "output_2",
      "input_type": "button",
      "end_reason": null,
      "occurred_at": "2026-08-26T15:47:23+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "7813"
}
```

## Opt-outs

Who has opted out of messaging, on which channel, when, and how. Your suppression list, in a form you can export and keep.

### GET /opt-outs

List opt-outs, most recent first, across every channel.

Query parameters

- channelstringoptional `instagram`, `facebook`, `whatsapp`, `telegram` or `sms`.
- statusstringoptional `opted_out` or `opted_in`. Only SMS can be opted back in, so `status=opted_in` returns SMS records only.
- scopestringoptional `comment_dm`, `broadcast` or `all`. Meta channels only, so passing it excludes SMS.
- sinceISO-8601optional Filters on `recorded_at`, the same field the list is sorted by, so someone who opts back in today shows up in today's sweep even if their original opt-out was long ago. Use it to sync consent changes incrementally.
- limit · starting_afterpaginationoptional

> **Note**
>
> **`id` is a string here**, prefixed by source (`"meta:13"`, `"sms:32"`), because Meta and SMS opt-outs are recorded separately and their numbering is independent. Treat `next_cursor` as opaque on this endpoint. SMS records return `method` and `keyword` as `null`: a carrier-level stop reaches us without the wording the person used, and we would rather say nothing than guess.

**Request**

```
curl "https://sumgenius.ai/api/v1/opt-outs?channel=instagram" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200 · the opt-out resource**

```
{
  "data": [
    {
      "id": "meta:13",
      "channel": "instagram",
      "contact_id": 20668,
      "identifier": "17841400000000000",
      "status": "opted_out",
      "scope": "all",
      "method": "dm_reply",
      "keyword": "STOP",
      "opted_out_at": "2026-07-26T21:04:07+00:00",
      "opted_in_at": null,
      "recorded_at": "2026-07-26T21:04:07+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## Team

### GET /team

Your team members, ordered by name, in one page. A member's `id` is what `assigned_to` references and what [assign](#assign-conversation) accepts. Credentials and permission data are never exposed.

- roleenum `agent` · `manager`
- statusenum `online` · `away` · `busy` · `offline`

**Request**

```
curl https://sumgenius.ai/api/v1/team \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response 200**

```
{
  "data": [
    {
      "id": 90011,
      "name": "Sam Ortiz",
      "email": "sam@example.com",
      "role": "manager",
      "status": "online",
      "avatar_url": null,
      "active_conversations": 1,
      "total_resolved": 0,
      "created_at": "2026-07-22T06:16:06+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## AI configuration

Every settings tab of the AI Configuration screen, as sections of one resource. Read all of it in one call, change any subset. The Claude setup helper, the magic wand, and the test bot are portal tools and are not part of the API.

### GET /ai/config

Eight sections. `available` on `file_handling`, `followups`, `csat` and `comment_ai` tells you whether the plan includes that feature. Needs the `read` scope.

- identity `name`, `tone` (casual, professional, formal), `instructions` (up to 15,000 characters), `instructions_source` (auto or manual, read-only), `welcome_message` (up to 500), `welcome_mode` (combined or standalone), `website_url`, `business_contact_email`, `business_contact_phone`.
- behavior `goals` (any of answer_questions, book_appointments, capture_leads, qualify_prospects), `batch_messages`, `batch_window_ms` (minimum 5000), `auto_escalation`, `copilot_enabled`, `business_hours` (seven days, read-only for now).
- restrictions `off_limits`: legal, medical, competitors, pricing, personal, offtopic, each true or false. `solicitation_pause`.
- starters `instagram.enabled` and `instagram.questions` (up to 4, 80 characters each), `facebook.enabled`, `facebook.mode` (ice_breakers or get_started), `facebook.questions`. `published` carries when each platform was last published and any error, read-only.
- file_handling `enabled` (one switch for images, documents and videos), `auto_escalate_on_image`, `auto_escalate_on_document`, `auto_escalate_on_video`, `escalate_if_team_online`. Enabling it forces batching on.
- followups `enabled`, `first_delay_minutes` (5 to 60), `second_delay_hours` (1 to 23), `quiet_hours.enabled`, `quiet_hours.start`, `quiet_hours.end` (HH:MM).
- csat `enabled`, `template` (simple, friendly, professional, custom), `custom_message` (up to 500).
- comment_ai The AI Comment Manager, which handles comments no trigger claimed. `enabled`, `mode` (`auto` posts replies at once, `approval` queues them under [/comments](#comments)), `instructions` (up to 5000), `reply_length` (short, medium, detailed), `moderation` (spam, profanity, negativity, competitor_links, self_promotion, each true to hide), `dm_on_hide` and `dm_message` ({name} is the commenter), `exclude_rules` (a list of `{type: post_id | keyword, value}`, replaced whole). Pro plans and above.

**Request**

```
curl https://sumgenius.ai/api/v1/ai/config \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
```

**Response (abridged)**

```
{
  "data": {
    "identity": {
      "name": "Aria",
      "tone": "casual",
      "instructions": "Aria persona and core guidance. ...",
      "instructions_source": "auto",
      "welcome_message": null,
      "welcome_mode": "combined",
      "website_url": "https://sumgenius.ai",
      "business_contact_email": "info@sumgenius.ai",
      "business_contact_phone": "833-365-7318"
    },
    "behavior": {
      "goals": ["answer_questions", "book_appointments"],
      "batch_messages": false,
      "batch_window_ms": 7000,
      "auto_escalation": true,
      "copilot_enabled": false,
      "business_hours": [ { "day": "sunday", "closed": true, "open": null, "close": null }, ... ]
    },
    "restrictions": { "off_limits": { "legal": true, ... }, "solicitation_pause": true },
    "starters": { ... },
    "file_handling": { "available": true, "enabled": false, ... },
    "followups": { "available": true, "enabled": false, ... },
    "csat": { "available": true, "enabled": false, ... },
    "comment_ai": { "available": true, "enabled": false, "mode": "approval", ... }
  }
}
```

### PATCH /ai/config

Send any subset of sections, each with any subset of its fields. Everything you leave out keeps its current value. Returns the full configuration after the change. Needs the `admin` scope.

- Validation Same rules as the portal. Out-of-range values are clamped where the portal clamps them (welcome message, batch window, name) and rejected where the portal rejects them (instructions over 15,000 characters, follow-up delays, starters without questions, an unknown CSAT template). An unknown section or field is `400 invalid_param` with `param` naming it.
- Plan gates `file_handling`, `followups`, `csat` and `comment_ai` return `403 feature_unavailable` on a plan that does not include them. Check `available` on the GET first.
- Starters publish on save Changing `starters` publishes to Instagram and Facebook at once, exactly like the portal. A publish failure still saves locally and is reported in `starters.published.*_error`.
- What this never touches The AI on/off switch per channel. Saving configuration leaves those alone; see channel switches.

**Request**

```
curl -X PATCH https://sumgenius.ai/api/v1/ai/config \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "identity": { "tone": "professional", "name": "Aria" },
    "behavior": { "goals": ["answer_questions", "capture_leads"] },
    "restrictions": { "off_limits": { "pricing": false } }
  }'
```

**403 feature_unavailable**

```
{
  "error": {
    "code": "feature_unavailable",
    "message": "AI Follow-ups are not available on this plan.",
    "param": "followups"
  }
}
```

### GET /ai/channels

The on/off switches per channel. Four channels: `meta` (Facebook and Instagram share one switch), `sms`, `whatsapp`, `telegram`. For each: `connected`, whether the channel is set up on the account; `ai_enabled`, whether the AI answers customers on it; `automation_enabled`, whether flows and triggers run on it. Needs the `read` scope.

**Response**

```
{
  "data": {
    "meta": { "label": "Facebook + Instagram", "connected": true, "accounts": { "facebook_page_id": null, "instagram_id": "17841400000000000" }, "ai_enabled": true, "automation_enabled": true },
    "sms": { "label": "SMS", "connected": false, "ai_enabled": false, "automation_enabled": false },
    "whatsapp": { ... },
    "telegram": { ... }
  }
}
```

### PATCH /ai/channels/{channel}

Switch the AI or automations on one channel. Send `ai_enabled` and/or `automation_enabled` as booleans. Returns that channel's state. Needs the `admin` scope. `facebook` and `instagram` are accepted as aliases of `meta`.

- Off always works Turning a switch off never fails. This is the call to make when the bot must stop answering right now.
- SMS AI on has two gates `409 sms_not_ready` until carrier registration assigns a Messaging Service to your number. `409 purge_active` while inbound body purge is on in your webhook settings, because the purge clears the SMS context the AI needs. Turn the purge off first.
- What this never touches Any other setting. The AI configuration, external reply mode and the purge flag are separate.

**Request**

```
curl -X PATCH https://sumgenius.ai/api/v1/ai/channels/meta \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"ai_enabled": false}'
```

**Response**

```
{
  "data": {
    "channel": "meta",
    "label": "Facebook + Instagram",
    "connected": true,
    "accounts": { "facebook_page_id": null, "instagram_id": "17841400000000000" },
    "ai_enabled": false,
    "automation_enabled": true
  }
}
```

## Knowledge base

What the AI knows: FAQs, services with pricing, and uploaded documents. One plan cap covers all three. Every write here is what the Knowledge tab does, including re-indexing for retrieval, so a change is live in the bot's answers within moments.

### GET /knowledge

Items against the plan cap, broken down by FAQs, services and documents, plus document storage. Check `items.can_add` before creating. Needs the `read` scope.

**Response**

```
{
  "data": {
    "items": { "current": 25, "limit": 100, "remaining": 75, "can_add": true, "breakdown": { "faqs": 13, "services": 12, "documents": 0 } },
    "document_storage": { "available": true, "current_mb": 0, "limit_mb": 50 }
  }
}
```

### /knowledge/faqs

`GET` lists (active only unless `include_inactive=true`), `POST` creates, `GET /{id}`, `PATCH /{id}` and `DELETE /{id}` do what they say. Writes need the `write` scope.

- questionstring, up to 1,000required on create What a customer would ask.
- answerstring, up to 10,000required on create What the AI should say. Written in the bot's voice by the model, so plain facts work best.
- keywordsstringoptional Comma-separated hints that help matching. Empty string clears.
- activebooleanoptional Switch the FAQ off without deleting it. Changing only `active` does not re-index; changing content does.

Create returns `403 plan_limit_reached` at the cap. Responses to create and update carry `embedding_pending`, true when indexing has not finished yet.

**Create**

```
curl -X POST https://sumgenius.ai/api/v1/knowledge/faqs \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"question": "Do you offer gift cards?", "answer": "Yes, in any amount from $25, at the front desk or online."}'
```

**Response**

```
{
  "data": {
    "id": 412,
    "question": "Do you offer gift cards?",
    "answer": "Yes, in any amount from $25, at the front desk or online.",
    "keywords": null,
    "category_id": null,
    "active": true,
    "usage_count": 0,
    "has_embedding": true,
    "source": "manual",
    "created_at": "2026-09-05T04:10:00+00:00",
    "embedding_pending": false
  }
}
```

### /knowledge/services

Same five operations as FAQs. A service is something the AI can quote and, with booking on, offer.

- namestring, up to 200required on create
- pricenumber, zero or morerequired on create Returned as a two-decimal string. `price_unit` is `fixed`, `hourly`, `per_unit` or `quote`. `currency` is an ISO 4217 code, or null for the account default.
- description · duration_minutes · display_order · category_id · activeoptional
- is_virtualbooleanoptional Bookings for this service get a Google Meet link. Honoured only when Google Calendar is connected and Meet is switched on; otherwise stored as false.
- image_urlread-only Service images are added in the portal. The API never changes or removes one.

**Create**

```
curl -X POST https://sumgenius.ai/api/v1/knowledge/services \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name": "Deep tissue massage", "price": 120, "currency": "USD", "duration_minutes": 60, "description": "Firm pressure, targeted at knots and chronic tension."}'
```

**Switch one off**

```
curl -X PATCH https://sumgenius.ai/api/v1/knowledge/services/88 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"active": false}'
```

### /knowledge/documents

Uploaded PDFs and text files. `GET` lists and `GET /{id}` reads metadata: name, type, size, embedding state. Content is never returned. `PATCH /{id}` takes `name` and `active`. `DELETE /{id}` removes the document and its embeddings.

There is no create. Uploading a document means parsing a file against a storage cap, which stays in the portal.

**Document**

```
{
  "id": 31,
  "name": "Spring menu.pdf",
  "type": "pdf",
  "url": null,
  "active": true,
  "size_bytes": 48213,
  "embedding_status": "completed",
  "embedding_count": 14,
  "last_embedded_at": "2026-08-30T17:02:11+00:00"
}
```

### GET /knowledge-gaps

Questions the AI could not answer well. Each gap carries the customer's message, the AI's reply, a generic form of the question so repeats group together, how often it came up, and where the classifier thinks the fix belongs: a new FAQ or a line in the instructions. Open gaps by default. Needs the `read` scope.

Read-only on purpose. Fix a gap with [POST /knowledge/faqs](#knowledge-faqs) or by updating `identity.instructions` in the [AI configuration](#update-ai-config). Dismissing stays in the portal.

Query parameters

- statusstringoptional `open` (default), `pending`, `resolved`, `dismissed`, or `all`.
- signal_typestringoptional `no_knowledge`, `weak_match`, `negative_feedback` or `escalation`.
- channel · since · limit · starting_afteroptional `since` filters on `last_seen_at`.

**Gap**

```
{
  "id": 832,
  "status": "open",
  "signal_type": "no_knowledge",
  "channel": "instagram",
  "conversation_id": 236067,
  "user_message": "can you confirm my 4:15 demo tomorrow?",
  "ai_response": "I can't see bookings from here, but our team can confirm shortly.",
  "canonical_question": "Can you confirm my booking?",
  "top_similarity": 0.41,
  "occurrence_count": 3,
  "suggested_answer": "If you cannot confirm a booking from the chat, tell the customer a person will confirm by message within the hour.",
  "suggested_target": "instructions",
  "last_seen_at": "2026-08-05T05:27:46+00:00"
}
```

## Media library

Images and short videos the AI can send in a reply. Creator plans and above; other plans get `403 feature_unavailable`.

### /media

`GET /media` lists every item with its public URL. `POST /media` uploads one as `multipart/form-data`: the file in a field named `file`, plus `name` (required), `description` and `category`. `PATCH /media/{id}` changes name, description or category. `DELETE /media/{id}` removes the item and its file. Writes need the `write` scope.

- Limits JPEG, PNG, MP4 or MOV. 8 MB for images, 25 MB for videos. 20 items per library; the 21st is `403 plan_limit_reached`.
- categorystring `transformation`, `testimonial`, `before_after`, `product`, `pricing`, `general`. The AI uses the category and description to pick the right item for a reply.

**Upload**

```
curl -X POST https://sumgenius.ai/api/v1/media \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -F "file=@before-after.jpg" \
  -F "name=Kitchen before and after" \
  -F "category=before_after"
```

**Media item**

```
{
  "id": 17,
  "name": "Kitchen before and after",
  "description": null,
  "category": "before_after",
  "url": "https://sumgenius.ai/uploads/media-library/50/a1b2c3.jpg",
  "mime_type": "image/jpeg",
  "kind": "image",
  "size_bytes": 184220,
  "width": 1200,
  "height": 900,
  "usage_count": 0,
  "created_at": "2026-09-05T05:00:00+00:00"
}
```

## Blocked users

People the AI never answers. Their messages still arrive and are stored; the bot just stays silent.

### /blocked-users

`GET /blocked-users` lists active blocks. `POST /blocked-users` blocks someone: send exactly one of `ig_username` (an Instagram handle, with or without the @, works before they have ever messaged you) or `conversation_id` (the person behind an existing conversation on Facebook or Instagram). `DELETE /blocked-users/{id}` unblocks; the AI answers their next message. Writes need the `write` scope.

Blocking someone already on the list is `409 already_blocked`. The block row is kept after an unblock for audit, so ids are never reused.

**Block by handle**

```
curl -X POST https://sumgenius.ai/api/v1/blocked-users \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"ig_username": "@spam.account"}'
```

**Blocked user**

```
{
  "id": 41,
  "channel": "instagram",
  "platform_user_id": null,
  "ig_username": "spam.account",
  "display_name": null,
  "conversation_id": null,
  "source": "manual",
  "blocked_at": "2026-09-05T05:00:00+00:00"
}
```

### /comment-triggers

A comment trigger watches comments on your posts, stories or Instagram Lives for keywords. When one matches it can post a public reply, send a DM with up to three buttons, hide the comment, tag the person, and start a flow. Standard CRUD: `GET /comment-triggers` (filters `platform`, `source_type`, `active`, `post_id`), `POST`, `GET /comment-triggers/{id}`, `PATCH` (partial), `DELETE`, plus `POST /comment-triggers/{id}/duplicate` (send `{}` as JSON; a bodiless POST is refused) and `GET /comment-triggers/{id}/stats`. Writes need the `write` scope.

Fields

- keywords · match_type · exclude_keywordsarray · string · array Any keyword matches. `match_type` is `contains` (default, anywhere in the text), `exact` (whole word) or `fuzzy` (tolerates a typo). An excluded word anywhere in the comment stops the trigger.
- platform · source_type · post_idstring · string · string `platform` is `instagram`, `facebook` or `both`. `source_type` is `comment` (posts and Reels), `story` (story replies), `both`, or `live`; story and live are Instagram only. A `post_id` from [GET /posts](#list-posts) scopes the trigger to one post; null means every post.
- response_mode · public_replies · dm_templatesstring · array · array `both`, `public_only` or `dm_only`. Up to five variations each; one is picked per fire. Set `dm_use_ai` with a `dm_ai_goal` to have the AI write the DM from your knowledge base instead of templates.
- buttonsarray, 1 to 3 Required for any DM mode: Meta needs a button on a comment-triggered DM. Each is `{title, type, response | url}`; titles are 20 characters max, `text` buttons send `response` when tapped, `url` buttons open `url`.
- activeboolean, PATCH only `true` activates, `false` pauses (and pauses linked flows). Plans cap how many triggers are live at once; creating past the cap saves the trigger paused with `created_paused: true`, and activating past it is `403 plan_limit_reached`. Activating a trigger whose linked flow is paused, broken, or one of several is `409 flow_conflict`.
- linked_flowsread-only Flows that start from this trigger, with `entry` `immediate` (starts when the trigger fires) or `button` (starts when the person taps a trigger button). A flow can also match comment keywords of its own with no trigger; that shows on [GET /flows](#list-flows) as a source with `mode: native` and is not a comment trigger.

> **Warning**
>
> **Changing buttons on a linked trigger pauses things.** If you change a button's title or type on a trigger that has linked flows, those flows and the trigger are paused so the flow branches can be reviewed; the response carries `flow_impact`. Other edits re-sync the flows silently. Deleting a trigger detaches and pauses its flows.

The AI Comment Manager, which answers public comments with your knowledge base when no trigger fires, is a separate feature and is not a trigger.

**Create**

```
curl -X POST https://sumgenius.ai/api/v1/comment-triggers \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Guide",
    "platform": "instagram",
    "keywords": ["guide", "link"],
    "response_mode": "both",
    "public_replies": ["Sent you a DM {name}!"],
    "dm_templates": ["Here is the guide."],
    "buttons": [{"title": "Open guide", "type": "url", "url": "https://example.com/guide"}]
  }'
```

**Comment trigger**

```
{
  "id": 212,
  "name": "Guide",
  "active": true,
  "platform": "instagram",
  "source_type": "comment",
  "post_id": null,
  "keywords": ["guide", "link"],
  "match_type": "contains",
  "exclude_keywords": [],
  "response_mode": "both",
  "public_replies": ["Sent you a DM {name}!"],
  "dm_templates": ["Here is the guide."],
  "dm_use_ai": false,
  "buttons": [{ "title": "Open guide", "type": "url", "url": "https://example.com/guide" }],
  "hide_comment_after_dm": false,
  "tag_id": null,
  "linked_flows": [],
  "stats": { "fires": 0, "dms_sent": 0, "public_replies": 0, "last_fired_at": null },
  "created_paused": false,
  "created_at": "2026-09-05T18:00:00+00:00"
}
```

### GET /comment-triggers/simulate

A dry run. Pass `text`, `platform` and optionally `post_id` and `source_type` as query parameters, and get back what would happen: the trigger that fires and what it would send, the linked flow that would start, or the flow with native keywords that would answer if no trigger fires. Every trigger on the account is listed under `candidates` with the reason it did not fire (`inactive`, `platform`, `source_type`, `post_scope`, `excluded_keyword`, `no_keyword_match`, `earlier_trigger_wins`). It runs the same matcher as live traffic, sends nothing and records nothing. Read scope.

`outcome` is `trigger_fired`, `native_flow`, `no_match`, or `automation_off` when the account's Meta automation switch is off. Per-person rules (opt-out, blocked, once per user) are not simulated.

**Request**

```
curl -G https://sumgenius.ai/api/v1/comment-triggers/simulate \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  --data-urlencode "platform=instagram" \
  --data-urlencode "text=Can I get the guide?"
```

**Response 200**

```
{
  "outcome": "trigger_fired",
  "automation_enabled": true,
  "trigger": {
    "id": 212, "name": "Guide", "matched_keyword": "guide",
    "actions": ["public_reply", "dm"],
    "public_replies": ["Sent you a DM {name}!"],
    "dm": { "type": "template", "templates": ["Here is the guide."] },
    "buttons": [{ "title": "Open guide", "type": "url", "url": "https://example.com/guide" }]
  },
  "linked_flow": null,
  "native_flow": null,
  "candidates": [
    { "id": 198, "name": "Pricing", "would_fire": false, "reason": "no_keyword_match" },
    { "id": 212, "name": "Guide", "would_fire": true, "reason": null, "matched_keyword": "guide" }
  ]
}
```

### GET /posts

Recent posts and Reels on the connected account, newest first, fetched live from Meta. Use a post's `id` as `post_id` to scope a trigger to it. `platform` is `instagram` (default) or `facebook`; `limit` up to 50; `starting_after` takes the returned `next_cursor`. `409 not_connected` when that channel is not connected, `503 service_unavailable` when Meta does not answer.

**Post**

```
{
  "id": "17912345678901234",
  "channel": "instagram",
  "caption": "New guide is out. Comment GUIDE.",
  "media_type": "IMAGE",
  "permalink": "https://www.instagram.com/p/example/",
  "thumbnail_url": "https://scontent.cdninstagram.com/...",
  "posted_at": "2026-09-04T22:15:00+00:00"
}
```

### /comments

The AI Comment Manager handles post and Reel comments that no [comment trigger](#comment-triggers) claimed. For each one it checks your exclusion rules, hides the comment if moderation flags it (spam, profanity, negativity, competitor links, self-promotion, whichever you turned on, with an optional DM to the person), writes a reply from your knowledge base, and either posts it (auto mode) or queues it for a person to approve (approval mode). This resource is that log and that queue. Settings are the `comment_ai` section of [/ai/config](#get-ai-config). Pro plans and above; below that every route is `403 feature_unavailable`.

`GET /comments` lists handled comments, newest first, kept 30 days. Filters: `status`, `channel`, `post_id`, `since`. `status=pending` is the approval queue. `GET /comments/stats` gives the queue depth and today's and this month's counts against the plan's caps.

The approval queue

- POST /comments/{id}/approvewrite Posts the pending reply publicly under the comment, as your account. Send `{"text": "..."}` to post your own wording instead of the AI draft; the draft is kept and `reply.edited` becomes true. Send `{}` to post the draft as is. Only a pending comment can be approved (`409 not_pending`). If Meta refuses the reply the row becomes `failed` and you get `502 send_failed`.
- POST /comments/{id}/rejectwrite Drops the pending draft. Nothing is posted. Body `{}`.
- POST /comments/approve · /comments/rejectwrite, bulk Body `{"ids": [...]}`, up to 100. Approve returns `approved`, `failed` and one error line per failed id; reject returns how many changed. Ids that are not pending are skipped, never an error.

status

`pending` a draft is waiting for approval · `sent` the reply was posted · `moderated` the comment was hidden · `rejected` a person dropped the draft · `failed` Meta refused the reply, or the AI could not draft one · `skipped` the plan's daily or monthly cap was reached.

Every state change also fires the [comment.handled](https://sumgenius.ai/docs/webhook-events#comment-handled) webhook, so a queue can be worked from Slack or a help desk without polling.

**Approve with an edit**

```
curl -X POST https://sumgenius.ai/api/v1/comments/57/approve \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"text": "Thanks! Prices are in the link in our bio."}'
```

**Handled comment**

```
{
  "id": 57,
  "status": "sent",
  "channel": "instagram",
  "post_id": "17912345678901234",
  "comment": { "id": "17987654321098765", "text": "How much is this?", "commenter_id": "17841400123456789", "commenter_username": "janedoe" },
  "sentiment": { "label": "neutral", "score": 0.1 },
  "moderation": { "action": "none", "flags": [], "dm_status": "none", "dm_text": null },
  "reply": {
    "draft": "Hi! Pricing is in the link in our bio, or DM us for details.",
    "sent_text": "Thanks! Prices are in the link in our bio.",
    "edited": true,
    "sent_at": "2026-09-05T19:02:11+00:00",
    "meta_reply_id": "17999999999999999"
  },
  "reviewed_by": "API key 6",
  "reviewed_at": "2026-09-05T19:02:11+00:00",
  "created_at": "2026-09-05T18:40:00+00:00"
}
```
