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+ planMachine-readable spec
Everything on this page, as an OpenAPI 3.1 document: 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.
Using an AI assistant instead of writing code? The MCP server wraps this API as 50 tools for Claude, Cursor and any MCP client.
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, and webhooks. 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.
https://sumgenius.ai/api/v1
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 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 |
{
"data": [ ... ],
"has_more": true,
"next_cursor": "18son403"
}
{
"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 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.
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"}'
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.
RateLimit-Limit: 200
RateLimit-Remaining: 182
RateLimit-Reset: 1785456060
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, write values through PATCH /contacts/{id}.
// 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.
curl https://sumgenius.ai/api/v1/account \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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"
}
]
}
}
}
Webhooks & keys
What the portal's Webhook & API page does, for a program. A person creates the first key in the portal; from there an integration can register the endpoint it wants events delivered to, test it, rotate its secret, and mint narrower keys for its own components. Writes need the admin scope. The wire format (envelope, signature) is not configurable here.
GET /webhooks/endpoints · POST /webhooks/endpoints · GET PATCH DELETE /webhooks/endpoints/{id} · POST …/{id}/rotate-secret · POST …/{id}/test
An endpoint is a URL you own that receives events. Up to three per account. Each has its own signing secret.
Create body
-
name · urlrequired
url must be https on a public host.
-
eventsarrayoptional
Event names to deliver. [] or omitted means every event. Names come from the events reference or x-webhook-events in the OpenAPI document.
-
governanceobjectoptional
The data-sharing declaration: attested, purpose (at least 8 characters), destination_owner, payload_profile (full carries message text, minimal omits it). Partial on PATCH.
Going live is a separate step. An endpoint is created paused. PATCH {"enabled": true} turns it on, and is refused with 422 governance_incomplete until the declaration is attested with a purpose. Pausing stops new events from queueing; anything already queued waits and delivers when you enable it again.
Secrets. signing_secret is returned once on create and once on rotate, never on read. After a rotation the previous secret keeps verifying for about 24 hours.
Test. POST …/{id}/test with {"channel": "meta"} queues a sample event and delivers it right away, paused or not. delivered tells you whether your server answered 2xx.
curl -X POST https://sumgenius.ai/api/v1/webhooks/endpoints \
-H "X-SumGenius-Api-Key: sgwh_your_admin_key" \
-H "Content-Type: application/json" \
-d '{
"name": "CRM sync",
"url": "https://crm.example.com/hooks/chatgenius",
"events": ["lead.captured", "contact.updated", "appointment.booked"],
"governance": {"attested": true, "purpose": "Sync leads and bookings into our CRM", "destination_owner": "Example Co", "payload_profile": "minimal"}
}'
{
"id": 12,
"name": "CRM sync",
"url": "https://crm.example.com/hooks/chatgenius",
"enabled": false,
"events": ["lead.captured", "contact.updated", "appointment.booked"],
"governance": { "attested": true, "complete": true, "purpose": "Sync leads and bookings into our CRM", "destination_owner": "Example Co", "payload_profile": "minimal" },
"timeout_ms": 10000, "max_attempts": 6, "secret_rotated_at": null,
"stats": { "delivered": 0, "failed": 0, "last_delivered_at": null },
"signing_secret": "whsec_…"
}
curl -X PATCH https://sumgenius.ai/api/v1/webhooks/endpoints/12 \
-H "X-SumGenius-Api-Key: sgwh_your_admin_key" \
-H "Content-Type: application/json" -d '{"enabled": true}'
GET /api-keys · POST /api-keys · DELETE /api-keys/{id}
Up to ten active keys per account. The list shows every key with a preview and its scope; is_current marks the one making the call. Key material is never listed.
Create takes name and scope (read by default). The plaintext comes back once as api_key. A key can mint keys up to its own reach: a full key can mint any scope, an admin key can mint read, write or admin.
Revoke stops the key at once. A key cannot revoke itself (409 cannot_revoke_self); use another key or the portal.
curl -X POST https://sumgenius.ai/api/v1/api-keys \
-H "X-SumGenius-Api-Key: sgwh_your_admin_key" \
-H "Content-Type: application/json" \
-d '{"name": "Reporting bot", "scope": "read"}'
{
"id": 14,
"name": "Reporting bot",
"preview": "9f2c11ab",
"scope": "read",
"is_current": false,
"revoked": false,
"created_at": "2026-09-07T09:12:40+00:00",
"last_used_at": null,
"revoked_at": null,
"api_key": "sgwh_9f2c11ab…"
}
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
curl "https://sumgenius.ai/api/v1/contacts?stage=qualified&limit=25" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"data": [
{
"id": 19214,
"channel": "instagram",
"platform_user_id": "17845629031224067",
"name": "Riley Chen",
"email": "[email protected]",
"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.
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. Fields must already be defined.
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"]
}'
{
"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 webhook event.
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"
}'
{
"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.
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.
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.
This cannot be undone. To remove one thread and keep the person, use delete conversation instead.
Tag definitions and send-history audit records survive. Repeating the call is a safe 404.
curl -X DELETE https://sumgenius.ai/api/v1/contacts/19214 \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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}. Values appear in every contact resource and in full-profile webhook payloads, and every change fires a 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.
curl https://sumgenius.ai/api/v1/custom-fields \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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.
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"
}'
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.
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.
-
updated_sincetimestampoptional
-
limit · starting_afterpaginationoptional
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.
curl "https://sumgenius.ai/api/v1/conversations?status=escalated" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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": "[email protected]",
"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.
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 and conversation.assigned as appropriate.
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}'
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"}'
{
"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, or null to unassign. An unknown id is 400.
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.
This cannot be undone. The contact survives; to erase the person entirely use 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.
curl -X DELETE https://sumgenius.ai/api/v1/conversations/188892 \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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.
curl "https://sumgenius.ai/api/v1/conversations/188892/messages" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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 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; 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.
A failed cold send still creates the conversation and contact before the delivery attempt. Repeats reuse the same records; remove them with 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 |
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."
}'
{
"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": {
"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.
GET /tags
All tag definitions, ordered by name, in one page. name is what the contacts ?tag= filter matches.
curl https://sumgenius.ai/api/v1/tags \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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.
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.
curl -X DELETE https://sumgenius.ai/api/v1/tags/14 \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{"data": {"id": 14, "deleted": true}}
Appointments
Bookings made through the AI booking agent. 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
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.
curl "https://sumgenius.ai/api/v1/appointments?status=confirmed" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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": "[email protected]",
"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.
curl https://sumgenius.ai/api/v1/appointments/91772 \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
GET /appointments/availability
The open slots for one day: business hours minus existing bookings, stepped by the service duration plus the buffer, and never sooner than the minimum notice. Exactly what the booking bot offers and the portal picker shows. An empty list means the day is closed or full.
-
dateYYYY-MM-DDrequired
In the account timezone, returned as timezone.
-
service_typestringoptional
A bookable service name (see booking.bookable_services on /ai/config). Sets the slot length; unknown or omitted means 30 minutes.
-
city · statestringoptional
Location-specific availability for multi-location accounts.
{
"data": {
"date": "2026-09-10",
"timezone": "America/Los_Angeles",
"service_type": "ChatGenius Setup",
"duration_minutes": 60,
"slots": [
{ "time": "09:00", "label": "9:00 AM" },
{ "time": "10:15", "label": "10:15 AM" }
]
}
}
POST /appointments
Book a slot. Same engine as the portal calendar and the bot: the account's required customer fields are enforced, the time must be in the future and free, the appointment is confirmed at once, it is synced to Google Calendar and GoHighLevel when connected, and appointment.booked fires. Needs the write scope.
-
date · timeYYYY-MM-DD · HH:MMrequired
The customer's wall-clock time in timezone (the account timezone when omitted). Use a time from availability.
-
customerobjectrequired
{name, email, phone, address}. name is always required; the rest follow the account's booking.required_fields. Missing ones are 422 missing_fields with the list.
-
conversation_idintegeroptional
Book for the person you are chatting with. Sets the channel, links the contact, and sends them the confirmation DM unless send_confirmation is false. Without it the booking is a walk-in with no DM.
-
service_type · duration_minutes · location · vehicle_info · cost · notesoptional
duration_minutes defaults to the service's duration from the service menu, else 30.
A taken or out-of-hours slot is 409 slot_unavailable. A past time is 400.
curl -X POST https://sumgenius.ai/api/v1/appointments \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-09-10",
"time": "10:15",
"service_type": "ChatGenius Setup",
"conversation_id": 236067,
"customer": {"name": "Jared", "email": "[email protected]"}
}'
{ "data": { "id": 92150, "confirmation_code": "SG-K2M7QX", "status": "confirmed", "datetime": "2026-09-10T17:15:00+00:00", ... } }
PATCH /appointments/{id}
Edit details and/or status, the portal's edit form. Send only what changes: customer, location, service_type, duration_minutes, timezone, vehicle_info, cost, notes, status (pending, confirmed, completed, cancelled, no_show). Detail edits are pushed to the connected calendars; a status of cancelled removes the calendar event and fires the cancelled event. To move the time use reschedule, which checks the slot; this endpoint does not take a date.
The response carries sync_warning: true when a connected calendar refused the change. The appointment itself was saved.
curl -X PATCH https://sumgenius.ai/api/v1/appointments/92150 \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"notes": "Bring the login", "status": "completed"}'
POST /appointments/{id}/cancel
Sets the status to cancelled, removes the Google Calendar and GoHighLevel events, and fires appointment.cancelled. 409 already_cancelled if it already is. Send {} as the body. The row is kept; there is no delete.
curl -X POST https://sumgenius.ai/api/v1/appointments/92150/cancel \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" -d '{}'
POST /appointments/{id}/reschedule
Moves a pending or confirmed appointment to a new date and time (in the appointment's timezone) after checking the slot is free, updates the connected calendars, and fires appointment.rescheduled. When the booking came from a conversation the customer is told in the DM, like the bot does. 409 slot_unavailable when the slot is taken, 409 not_active when the appointment is cancelled, completed or a no-show.
curl -X POST https://sumgenius.ai/api/v1/appointments/92150/reschedule \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"date": "2026-09-11", "time": "14:00"}'
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 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. Read them, and build them: a flow is a JSON definition (one trigger, a list of nodes, the edges between their ports), and GET /flows/nodes is the catalogue that tells a person or an LLM exactly which node types, sources, config keys and enum values exist. Validate a definition, create it as a draft, then publish, activate or pause it. A flow created here opens in the visual builder like any other; positions are laid out automatically the first time.
This is also the parent of flow sessions: a session records the flow_id and flow_version it ran under.
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
Definitions are not in the list. GET /flows/{id} and GET /flows/{id}/definition return the definition. 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, 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%.
curl "https://sumgenius.ai/api/v1/flows?status=active" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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. The list resource plus definition (the draft when one exists, otherwise the published version), has_draft, draft_behavior_changed, node_count and tag_id. 404 if it does not exist, was deleted, or belongs to another account.
curl https://sumgenius.ai/api/v1/flows/178 \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
GET /flows/nodes
The node catalogue: everything a definition may contain. Read it once before building a flow; it is the contract the validator enforces.
-
definitionobject
The shape of a definition and the structural rules (one trigger with id "1", every other node reachable, ports must exist, node cap).
-
sourcesobject
The 13 trigger sources keyed by name (dm, comment, story_reply, ai_intent, first_message...). Each has channels it can fire on, needs_keywords, and its config schema.
-
nodesobject
The 12 node types keyed by type. Each has inputs, outputs (the port names, or the rule that derives them: a message gets one port per postback button plus output_timeout; a condition gets one per rule plus a fall-through), terminal, a description, and a JSON-Schema-style config with required keys and enums.
-
enums · placeholdersobject · array
Every allowed value (channels, match types, input types, rule types, action types, delay units, HTTP methods, button and media types) and the {placeholders} a message may use.
-
liveobject
This account's options: custom_fields, tags, services (for book_appointment), other flows (for goto_flow), comment_triggers (for a linked comment source), plus node_limit, flow_limit and active_flows from the plan.
{
"sources": {
"dm": { "label": "Keyword DM", "channels": ["facebook", "instagram", "whatsapp"], "needs_keywords": true, "config": { ... } },
"story_reply": { "label": "Story reply", "channels": ["instagram"], "needs_keywords": false, "config": { ... } }
},
"nodes": {
"collect_input": {
"label": "Collect input", "inputs": 1,
"outputs": { "output_1": "valid answer collected", "output_2": "max retries exhausted" },
"terminal": false,
"config": { "type": "object", "properties": { "question": { ... }, "input_type": { "enum": ["email", "phone", "name", "text", "number"] }, ... }, "required": ["question", "input_type", "variable_name"] }
}
},
"live": { "tags": [{ "id": 12, "name": "Hot lead" }], "node_limit": 25, "flow_limit": 10, "active_flows": 3 }
}
POST /flows/validate
Check a definition without saving anything. Runs the same checks as create and update: structure, every node's config against the catalogue, channel compatibility with the trigger source, keyword collisions with your other active flows, and references (tag ids, custom field keys, service ids, target flows, comment triggers) against this account. Always 200; read valid.
Body
-
definitionobjectrequired
{trigger, nodes, edges}. See the example under create.
-
channelsarrayoptional
Where the flow will run: facebook, instagram, whatsapp. Defaults to facebook and instagram.
{
"valid": false,
"errors": [
"Orphaned node: 3 (message) has no incoming edge.",
"Collect input node 2: input_type must be one of email, phone, name, text, number."
],
"warnings": [
"Node 2 (collect_input): port output_2 is not connected; the flow ends there."
]
}
POST /flows
Create a flow as a draft. Nothing runs until you publish or activate it. Returns the flow with 201 and any warnings; a definition with errors is refused with 422 invalid_definition and the same errors list validate returns. 403 plan_limit_reached when the plan's flow count is full.
Body
-
namestringrequired
Up to 100 characters.
-
definitionobjectrequired
trigger: the trigger config (source plus that source's keys: keywords and match_type for dm, comment_trigger_id or comment_keywords for comment, intent_description for ai_intent...). nodes: [{id, type, config}], ids are strings and the trigger is "1". edges: [{from, from_port, to}]; from_port defaults to output_1.
-
channelsarrayoptional
Defaults to facebook and instagram. Must be channels the trigger source can fire on.
-
tag_idintegeroptional
Tag applied to everyone who enters the flow. One of live.tags.
curl -X POST https://sumgenius.ai/api/v1/flows \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Guide request",
"channels": ["instagram"],
"definition": {
"trigger": { "source": "dm", "keywords": ["guide"], "match_type": "contains" },
"nodes": [
{ "id": "1", "type": "trigger", "config": { "source": "dm", "keywords": ["guide"], "match_type": "contains" } },
{ "id": "2", "type": "collect_input", "config": { "question": "Where should I send it?", "input_type": "email", "variable_name": "email", "max_retries": 2, "retry_message": "That does not look like an email." } },
{ "id": "3", "type": "action", "config": { "action_type": "tag_conversation", "tag_value": "guide" } },
{ "id": "4", "type": "message", "config": { "text": "Sent to {email}. Check your inbox." } }
],
"edges": [
{ "from": "1", "to": "2" },
{ "from": "2", "from_port": "output_1", "to": "3" },
{ "from": "3", "to": "4" }
]
}
}'
{
"id": 184,
"name": "Guide request",
"status": "draft",
"channels": ["instagram"],
"tag_id": null,
"version": 1,
"published_version": null,
"has_draft": true,
"node_count": 4,
"definition": { "trigger": { ... }, "nodes": [ ... ], "edges": [ ... ] },
"warnings": ["Node 2 (collect_input): port output_2 is not connected; the flow ends there."]
}
PATCH /flows/{id} · GET /flows/{id}/definition
Update name, channels, tag_id or definition. Sending a definition saves it as the flow's draft: the published version keeps running until you publish. A definition replaces the whole graph; send the complete one. The same validation as create applies (422 invalid_definition). GET /flows/{id}/definition returns {flow_id, has_draft, definition} on its own, the draft when one exists.
curl -X PATCH https://sumgenius.ai/api/v1/flows/184 \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"name": "Guide request (IG)", "tag_id": 12}'
POST /flows/{id}/publish · activate · pause · discard-draft · duplicate · DELETE /flows/{id}
The builder's buttons. Each POST takes an empty JSON body ({}) and returns the flow.
-
publish
Makes the draft the live definition and bumps published_version. A draft flow goes active when the plan's active-flow cap allows; otherwise it stays published but paused. A comment flow linked to a comment trigger goes live with that trigger.
-
activate
Sets the flow live, publishing the draft first when there is one. 403 plan_limit_reached at the active-flow cap; 422 invalid_definition when the draft has errors or its keywords collide with another active flow; 409 flow_blocked for anything else the builder would refuse.
-
pause
Stops the flow and cancels anyone mid-session. Only an active flow can be paused (409 not_active). Pausing a linked comment flow pauses its comment trigger too.
-
discard-draft
Throws away unpublished changes; the published definition stays.
-
duplicate
Copies the flow as a new draft named "... (Copy)". 201 with the copy.
-
DELETE
Deletes the flow for good: its versions go with it and anyone mid-session is dropped. A linked comment trigger is paused, not deleted. 200 {"deleted": true}.
Publishing, activating and pausing fire flow.published, flow.activated and flow.paused whether done here or in the portal.
curl -X POST https://sumgenius.ai/api/v1/flows/184/activate \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" -d '{}'
{
"error": {
"code": "plan_limit_reached",
"message": "You're at your live flow limit (3). Pause another flow before publishing this one."
}
}
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
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.
curl "https://sumgenius.ai/api/v1/flow-sessions?flow_id=178&status=completed" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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.
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.
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.
curl "https://sumgenius.ai/api/v1/flow-sessions/6075/events" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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
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.
curl "https://sumgenius.ai/api/v1/flow-events?flow_id=178&type=branch_selected" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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"
}
Analytics
Read-only numbers, computed by the same code the portal uses. Two windows everywhere: the current one and the one before it, so a change is always visible without a second call. No message content.
GET /flows/{id}/analytics
The builder's Analytics panel for one flow. Analytics are per published version: version defaults to the current one and versions[] lists the others, with has_tracked_runs so you know which are worth asking for. Only runs with journey tracking count; other_version_runs_excluded says how many runs in the window belong to other versions, and other_versions[] summarises them.
Query parameters
-
rangestringoptional
7d, 30d (default), 90d or all. comparison is null for all.
-
channelstringoptional
facebook, instagram or whatsapp.
-
versionintegeroptional
A published version from versions[]. 404 for a version the flow never had.
What comes back
-
summary · comparison · lifetime
Runs, completed, expired, cancelled, still active, completion rate and median seconds to complete for the window; the same for the previous window as deltas; and the flow's all-time counters across every version.
-
outcomes
What the runs produced: emails and phones collected, leads marked, appointments booked, links clicked, handoffs, custom fields saved. Each with a count, a rate against runs, and a median time to reach it.
-
per_node · branches · nodes
For every node of that version: how many runs reached it, continued past it, answered it, ended there and why (terminal_reasons), plus link clicks per button. branches gives the split per output port in the same order. nodes maps ids to types; the full definition is on GET /flows/{id}.
curl "https://sumgenius.ai/api/v1/flows/178/analytics?range=30d" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"flow_id": 178, "version": 4, "current_version": 4, "range": "30d", "channel": "all",
"summary": { "runs": 312, "completed": 188, "expired": 97, "cancelled": 6, "active": 21, "completion_rate": 64.6, "median_completed_seconds": 41 },
"comparison": { "previous_runs": 240, "runs_change_percent": 30, "previous_completion_rate": 58.1, "completion_rate_change_points": 6.5 },
"lifetime": { "runs": 1904, "completed": 1211, "not_completed": 693, "completion_rate": 63.6 },
"outcomes": [ { "key": "email_collected", "label": "Emails collected", "count": 171, "rate": 54.8, "median_seconds": 28 } ],
"per_node": [
{ "node_id": "2", "node_type": "message", "reached": 312, "continued": 240, "continued_rate": 76.9, "responses": 240, "loss_exits": 72, "terminal_reasons": { "response_timeout": 72 }, "buttons": { "postback_1": { "clicks": 201, "click_rate": 64.4 } } }
],
"branches": [ { "output_1": { "selected": 201, "selection_rate": 64.4 }, "output_2": { "selected": 39, "selection_rate": 12.5 } } ]
}
GET /analytics/overview
The numbers behind the portal's Overview page, from the same data layer: a rolling 30-day window against the 30 days before it. No parameters; the window is fixed like the page, and every count is a {current, previous} pair.
-
messages
Inbound, outbound and conversations touched, in total and by_channel.
-
replies
Outbound bot messages split into ai (written by the model) and automation (flows, comment triggers, welcomes).
-
flows · comment_triggers
Entries per flow and fires per standalone trigger, each with all_time. A comment trigger attached to a flow is the top of that flow's funnel, so its fires ride on the flow row as comment_trigger_fires and never appear as their own row. One comment is never counted twice.
-
contacts · appointments · knowledge_gaps · needs_you · api_sends
New contacts and totals with phone or email; appointments created, upcoming, and the next few; knowledge gaps by status; escalated conversations and Copilot drafts waiting; Send API requests this window.
-
usage
This billing cycle: conversations used against the plan cap, overage state, SMS used, and whether the account is on a trial. Account-level, never channel-scoped. null if the plan lookup fails, the way the page degrades.
{
"window": { "days": 30, "current_from": "2026-08-07T21:34:48+00:00", "previous_from": "2026-07-08T21:34:48+00:00" },
"messages": {
"inbound": { "current": 1412, "previous": 1180 }, "outbound": { "current": 1890, "previous": 1544 }, "conversations": { "current": 402, "previous": 351 },
"by_channel": { "instagram": { ... }, "facebook": { ... }, "sms": { ... } }
},
"replies": { "ai": { "current": 1105, "previous": 902 }, "automation": { "current": 785, "previous": 642 } },
"flows": [ { "id": 178, "name": "Comment to guide", "entries": { "current": 312, "previous": 240, "all_time": 1904 }, "comment_trigger_id": 90077, "comment_trigger_fires": { "current": 640, "previous": 515 } } ],
"contacts": { "new": { "current": 298, "previous": 233 }, "total": 4120, "with_phone": 1877, "with_email": 2410 },
"appointments": { "upcoming": 7, "created": { "current": 18, "previous": 14, "all_time": 69 }, "next": [ { "id": 92149, "channel": "instagram", "customer_name": "Maya R.", "scheduled_at": "2026-09-17T16:00:00+00:00", "service_type": "consultation", "status": "confirmed" } ] },
"knowledge_gaps": { "open": 3, "resolved": 12 },
"needs_you": { "escalated": 2, "copilot_drafts": 0 },
"api_sends": { "requested": 10, "sent": 10 },
"usage": { "conversations": { "used": 402, "limit": 1000, "percent": 40.2 }, "overage": { "enabled": false, "eligible": true, "units": 0 }, "sms": { "used": 0, "limit": 0 }, "billing_cycle": { "start": "2026-08-21", "end": "2026-09-20" }, "is_trial": false }
}
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
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.
curl "https://sumgenius.ai/api/v1/opt-outs?channel=instagram" \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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
}
GET /team/invitations · POST /team/invitations · POST /team/invitations/{id}/resend · DELETE /team/invitations/{id}
An invitation is how someone joins: they get an email with a setup link, choose a password, and appear in GET /team. Pending invitations hold a seat against the plan cap until accepted, expired (7 days) or cancelled.
Create body
-
emailstringrequired
-
namestringrequired
Used in the email greeting only. They set their own name when they accept.
-
rolestringoptional
agent (default) answers conversations. manager also gets the dashboard, leads, AI settings and content. Billing, team, integrations and keys stay owner-only.
-
channelsarray or nulloptional
Restrict them to some of facebook, instagram, whatsapp, sms, telegram. Omit for every channel.
403 plan_limit_reached when every seat is taken. 409 already_member or 409 invitation_pending for a known address. The setup link is never returned, except as invite_link when email_sent is false, so you can deliver it yourself.
Resend issues a fresh link (the old one stops working), resets the expiry and emails the reminder. Cancel removes the invitation and frees the seat.
curl -X POST https://sumgenius.ai/api/v1/team/invitations \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "name": "Sam", "role": "agent", "channels": ["instagram", "facebook"]}'
{
"id": 41,
"email": "[email protected]",
"role": "agent",
"channels": ["facebook", "instagram"],
"status": "pending",
"expires_at": "2026-09-14T21:10:03+00:00",
"created_at": "2026-09-07T21:10:03+00:00",
"email_sent": true
}
PATCH /team/{id} · DELETE /team/{id}
PATCH changes role and/or channels (null for every channel). It takes effect at their next login, the same as the portal. Returns the member. DELETE removes them: their login stops at once and their conversations become unassigned. conversations_unassigned says how many. No event fires for that.
curl -X PATCH https://sumgenius.ai/api/v1/team/90011 \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"role": "manager", "channels": null}'
{ "id": 90011, "object": "team_member", "deleted": true, "conversations_unassigned": 2 }
Team
The people who log in to your portal and can be assigned conversations. Reading is read scope. Inviting, changing roles or access, and removing need the admin scope, and do exactly what the portal's team page does for the account owner.
GET /team
Your team members, ordered by name, in one page. A member's id is what assigned_to references and what assign accepts. Credentials and permission data are never exposed.
-
roleenum
agent · manager
-
statusenum
online · away · busy · offline
-
channelsarray or null
The channels this member can see. null means every channel.
curl https://sumgenius.ai/api/v1/team \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"data": [
{
"id": 90011,
"name": "Sam Ortiz",
"email": "[email protected]",
"role": "manager",
"status": "online",
"avatar_url": null,
"active_conversations": 1,
"total_resolved": 0,
"channels": null,
"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
Nine 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), 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.
-
booking
The Booking Configuration modal. enabled (AI Booking; off means the AI declines booking requests, the API and the portal can still book), google_meet_invites (needs Google Calendar connected, forces email to required), required_fields (name, email, phone, address, vehicle, cost, each true or false, plus address_parts {city, state, street}). Read-only alongside: timing (buffer, minimum notice, max advance, reminder hours, timezone), calendar (which provider is connected), and bookable_services (the service menu rows with a duration, managed under /knowledge/services).
curl https://sumgenius.ai/api/v1/ai/config \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
{
"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": "[email protected]",
"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", ... },
"booking": { "enabled": true, "google_meet_invites": false, "required_fields": { "name": false, "email": false, ... }, "bookable_services": [ ... ], ... }
}
}
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.
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 } }
}'
{
"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.
{
"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.
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}'
{
"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.
{
"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.
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."}'
{
"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.
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."}'
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.
{
"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 or by updating identity.instructions in the AI configuration. 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.
{
"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.
curl -X POST https://sumgenius.ai/api/v1/media \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-F "[email protected]" \
-F "name=Kitchen before and after" \
-F "category=before_after"
{
"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.
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"}'
{
"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 /recent-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 as a source with mode: native and is not a comment trigger.
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.
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"}]
}'
{
"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.
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?"
{
"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 /recent-posts
Recent posts and Reels already on the connected Instagram or Facebook account, newest first, fetched live from Meta (not Content Studio drafts; those are /posts). 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.
{
"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 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. 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 422 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 webhook, so a queue can be worked from Slack or a help desk without polling.
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."}'
{
"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"
}
/posts
Content Studio: draft, schedule and publish posts to Instagram, Facebook and TikTok, with the same publisher the portal uses. The flow is: read capabilities, create the post, upload its media, then publish now or schedule. Content Studio plans only; below that every route is 403 feature_unavailable. Writes need the write scope.
Fields
-
platform · post_typestring · string
platform is a target set: instagram, facebook, tiktok, both (Instagram and Facebook), instagram_tiktok, facebook_tiktok, all. The response expands it as targets. post_type is feed_post, carousel, reels or story. TikTok takes feed posts and Reels only.
-
caption · hashtags · scheduled_at
Caption up to 2,200 characters; hashtags without the #. scheduled_at is ISO-8601; set it on create or PATCH to schedule, send null on PATCH to go back to draft. Scheduling needs a caption (stories excepted) and a future time.
-
advancedfirst_comment · collaborators · comment_auto_reply
Sent as top-level fields, returned under advanced. first_comment is posted under the post right after publish. collaborators are up to three Instagram usernames invited to co-own the post. comment_auto_reply {keywords, match_type, response_mode, public_replies, dm_templates, buttons} becomes a comment trigger scoped to this post when it publishes; any DM mode needs at least two buttons. None of the three apply to a TikTok-only post.
-
tiktokobject
The TikTok Settings block: privacy_level (must be one of the creator's options from capabilities), title (up to 150, defaults to the caption), allow_comments, allow_duet, allow_stitch, and the content disclosure: content_disclosure with promote_self and/or promote_third_party. Ignored unless TikTok is a target; null in the response otherwise.
-
statusread-only
draft · scheduled (the scheduler publishes it within a minute of scheduled_at) · publishing · published (see published_ids) · failed (see error_message, then retry). Only draft, scheduled and failed posts can be edited; published posts cannot be deleted.
The plan cap counts scheduled and published posts this month. A plain draft never holds a slot. A comment auto-reply on a scheduled post also needs a free live comment-trigger slot. Both are 403 plan_limit_reached.
curl -X POST https://sumgenius.ai/api/v1/posts \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"platform": "instagram_tiktok",
"post_type": "reels",
"caption": "Behind the scenes of the new setup.",
"hashtags": ["studio", "setup"],
"scheduled_at": "2026-09-12T16:00:00Z",
"first_comment": "Comment SETUP for the checklist.",
"tiktok": {"privacy_level": "PUBLIC_TO_EVERYONE", "allow_comments": true}
}'
{
"id": 90050,
"status": "scheduled",
"platform": "instagram_tiktok",
"targets": ["instagram", "tiktok"],
"post_type": "reels",
"caption": "Behind the scenes of the new setup.",
"hashtags": ["studio", "setup"],
"scheduled_at": "2026-09-12T16:00:00+00:00",
"published_at": null,
"media": [],
"advanced": { "first_comment": "Comment SETUP for the checklist.", "collaborators": [], "comment_auto_reply": null, "cover_offset_ms": null },
"tiktok": { "privacy_level": "PUBLIC_TO_EVERYONE", "title": null, "allow_comments": true, "allow_duet": true, "allow_stitch": true, "content_disclosure": false, "promote_self": false, "promote_third_party": false },
"published_ids": { "instagram_media_id": null, "facebook_post_id": null, "tiktok_video_id": null, "tiktok_publish_id": null },
"error_message": null,
"retry_count": 0,
"created_at": "2026-09-06T08:30:00+00:00"
}
GET /posts/capabilities
Read this before building a post. It says which of Instagram, Facebook and TikTok are connected, the TikTok creator's allowed privacy_level_options and video duration limit (fetched live from TikTok, the full list when TikTok is not connected or does not answer), the post types, and the plan's monthly post cap and storage cap with what is used.
{
"data": {
"platforms": { "instagram": { "connected": true }, "facebook": { "connected": false }, "tiktok": { "connected": true, "username": "knightsoundslv" } },
"post_types": ["feed_post", "carousel", "reels", "story"],
"tiktok": { "privacy_level_options": ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "SELF_ONLY"], "max_video_post_duration_sec": 600, "duet_disabled": false, "stitch_disabled": false, "comment_disabled": false },
"limits": { "posts_per_month": 100, "posts_used_this_month": 4, "storage_mb": 2048, "storage_used_mb": 131, "caption_max": 2200, "collaborators_max": 3, "carousel_items": { "min": 2, "max": 10 } }
}
}
/posts/{id}/media
POST /posts/{id}/media uploads one file as multipart/form-data in a field named file, with an optional sort_order. Call it once per item. JPEG, PNG or WebP up to 8 MB and 320 to 1440 px wide; MP4 or MOV up to 1 GB, 3 seconds to 15 minutes for Reels and up to 60 seconds for stories. A carousel takes 2 to 10 items. Uploads count against the plan's storage cap (403 plan_limit_reached). Every media call returns the post.
PATCH /posts/{id}/media with {"order": [mediaId, ...]} reorders; list every id. DELETE /posts/{id}/media/{mediaId} removes the file.
curl -X POST https://sumgenius.ai/api/v1/posts/90050/media \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-F "[email protected]" -F "sort_order=0"
/posts/{id}/publish · schedule · unschedule · retry
-
POST /posts/{id}/publishbody {}
Publishes to every target at once. Needs a caption (stories excepted), media for any Instagram target, and two or more items for a carousel. When a platform rejects it the post becomes failed and the call is 422 publish_failed with the post and per-platform results. post.published or post.failed fires either way.
-
POST /posts/{id}/schedule{"scheduled_at"}
Sets or moves the publish time of a draft, failed or scheduled post. The scheduler publishes it within a minute of that time and fires the same events. post.scheduled fires now.
-
POST /posts/{id}/unschedulebody {}
Scheduled back to draft.
-
POST /posts/{id}/retrybody {}
Publishes a failed post again, up to the retry cap.
Any of these on a post in the wrong status is 409 not_editable.
curl -X POST https://sumgenius.ai/api/v1/posts/90050/publish \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" -d '{}'
Market Intel
Competitor tracking and Instagram account insights, the data behind the portal's Market Intel page. Two things gate it. The account must be connected with Facebook Login, because Instagram-only login cannot read competitors, and every route answers 403 feature_unavailable otherwise. Competitor slots are capped by plan: 1 on Free, 3 on Creator, 5 on Professional, 10 on Business.
Everything except /market-intel/insights reads stored snapshots, so it is instant and spends no Meta quota. POST /market-intel/refresh is what writes those snapshots, and it is the only call that costs you Meta requests.
GET /competitors
Every account you track, with the numbers from its latest stored snapshot, plus how many slots your plan allows. The whole set comes back in one page: has_more is always false.
last_snapshot_date is null until the first refresh, and status is error when Meta last refused the lookup, which usually means the account went private or changed handle.
Beyond the usual envelope
- trackedinteger
Slots in use.
- max_trackedinteger
Your plan's cap.
- remaininginteger
What is left.
POST /competitors
Body is {"username": "nasa"}, with or without the leading @. The handle must be a public Instagram business or creator account: personal and private accounts are invisible to the API for everyone, and come back 422 invalid_param. At your plan's cap this is 403 plan_limit_reached, and an account you already track is 409 conflict. The first snapshot is taken immediately, so the response already carries follower and engagement numbers.
DELETE /competitors/{id}
Frees the slot. Snapshot history is kept, so re-adding the same handle later picks up where it left off rather than starting from zero.
curl -X POST https://sumgenius.ai/api/v1/competitors \
-H "X-SumGenius-Api-Key: sgwh_..." \
-H "Content-Type: application/json" \
-d '{"username": "nasa"}'
{
"data": {
"id": 22,
"username": "nasa",
"display_name": "NASA",
"is_verified": true,
"followers": 104383728,
"engagement_rate": 0.6412,
"last_snapshot_date": "2026-09-08"
}
}
GET /market-intel/leaderboard
Your account and every competitor ranked together, which is the one call to make for any "how do I compare" question. Each row carries followers, following, posts, engagement rate, average likes and comments, posts per week, follower growth per day and the content mix as percentages of reels, photos and carousels. Your own row has is_own_account: true, and your_rank saves you finding it.
Query parameters
- periodintegeroptional
7, 30 (default) or 90. Anything else is a 400.
GET /market-intel/history
Follower count and engagement rate over the window, one series per account, oldest point first, yours first in the list. The account label is the competitor's handle, or the literal you for your own. One point per day a snapshot exists, so gaps mean nobody refreshed that day. Same period parameter.
GET /market-intel/wins
The comparisons already computed against each competitor over the last 30 days, split into wins where you lead and opportunities where they do. Each carries a metric (engagement, growth, posting), the competitor it is measured against, and a message written for a person with the gap in it. No AI is involved; this is arithmetic on the snapshots.
{
"data": {
"period_days": 30,
"your_rank": 1,
"total_accounts": 3,
"accounts": [
{
"username": "sumgeniusai",
"is_own_account": true,
"followers": 39,
"engagement_rate": 14.3162,
"posts_per_week": 0.32,
"follower_growth_per_day": 0.1,
"content_mix": {
"reels": 4,
"photos": 37,
"carousels": 59
}
}
]
}
}
{
"metric": "engagement",
"competitor": "chatgpt",
"message": "Higher engagement than @chatgpt (+13.91%)"
}
GET /competitors/{id}
One competitor and your own account side by side, as the portal's comparison table. metrics[] holds a row per measure with you, them, the difference and you_lead. That last one is null for a metric with no better side, such as Following, so do not read a null as a loss. your_wins and competitor_wins count the decided rows.
Stored snapshots, no Meta call. A competitor id from another account is a 404, like everywhere else in this API.
{
"data": {
"competitor": { "username": "hawaiientrepreneurs" },
"metrics": [
{
"metric": "Engagement Rate",
"you": 14.3162,
"them": 0.1291,
"difference": 14.1871,
"you_lead": true
},
{
"metric": "Following",
"you": 6,
"them": 4859,
"difference": -4853,
"you_lead": null
}
],
"your_wins": 3,
"competitor_wins": 3
}
}
GET /market-intel/audience
The latest stored breakdown of your own followers. gender_percent and age_percent are shares, not counts, because that is all Instagram reports. top_countries and top_cities are lists of {country|city, count, percentage}, biggest first. online_followers_by_hour is what "when should I post" is answered from, and Instagram often omits it.
404 until the first refresh. Instagram withholds all of this for accounts under roughly 100 followers, which arrives as zeros and empty lists rather than an error.
GET /market-intel/top-posts
Your own recent posts with their stored performance: reach, impressions, likes, comments, saves, shares, video views, engagement rate and save rate, newest first. limit is 1 to 50, default 12. Stored, so refresh first if you need today's numbers.
GET /market-intel/insights
Reach, profile views, accounts engaged, interactions, saves, shares and follows for your own account. days is 1, 7 (default) or 30. This one calls Instagram live, so it takes a moment and counts against Meta's rate limit. Which metrics come back depends on what the account is eligible for, so read the object rather than assuming a fixed set. A connection made without the insights permission is 403 permission_required; reconnect in the portal to fix it.
{
"data": {
"snapshot_date": "2026-09-08",
"gender_percent": { "male": 49.2, "female": 36.2 },
"age_percent": { "25-34": 35.6 },
"top_countries": [
{ "country": "US", "count": 12166, "percentage": 96.6 }
],
"top_cities": [
{ "city": "Chicago, Illinois", "count": 3716, "percentage": 49.8 }
]
}
}
{
"data": {
"days": 7,
"metrics": {
"views": 20,
"reach": 2,
"profile_views": 8,
"accounts_engaged": 2,
"total_interactions": 5
}
}
}
POST /market-intel/refresh
Pulls current numbers from Meta for your account and every tracked competitor, then writes today's snapshot. Exactly what the portal's Refresh button does, and the source of everything the other routes read.
One Meta call per account, so it takes several seconds on a full slate. Five minutes are enforced between refreshes: too soon and you get 429 too_soon with retry_after_seconds telling you exactly how long to wait. Send {} as the body.
refreshed counts the competitors updated, errors[] names the ones Meta refused, usually because the account went private.
{
"error": {
"code": "too_soon",
"message": "Please wait 285 seconds before refreshing again.",
"retry_after_seconds": 285
}
}