ChatGenius Developer REST API Markdown

REST API v1

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

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

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.

Base URL
https://sumgenius.ai/api/v1
Authenticated request
curl https://sumgenius.ai/api/v1/account \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"

Envelope, errors & pagination

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

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

HTTPcodeWhen
400invalid_paramA parameter failed validation; param names it
400invalid_jsonBody is not valid JSON
404not_foundMissing, or not owned by your account
405 Wrong method; the Allow header lists valid ones
429rate_limitedREST request quota or burst ceiling reached
503service_unavailableTemporary safety or infrastructure failure; retry as directed
500server_errorUnexpected failure on our side
List envelope
{
  "data": [ ... ],
  "has_more": true,
  "next_cursor": "18son403"
}
Error envelope
{
  "error": {
    "code": "invalid_param",
    "message": "stage must be one of: new, contacted, qualified, customer, lost.",
    "param": "stage"
  }
}

Rate limits

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

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

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

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

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

Rate-limit headers
RateLimit-Limit: 200
RateLimit-Remaining: 182
RateLimit-Reset: 1785456060
Response 429
Retry-After: 1

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

Data conventions

  • timestampsUTC ISO-8601

    Everywhere, e.g. 2026-07-25T04:35:23+00:00. The updated_since filters accept the same format.

  • channelenum

    facebook · instagram · sms · telegram · whatsapp

  • stageenum

    new · contacted · qualified · customer · lost

  • statusenum

    active · resolved · pending · archived · escalated (conversations)

  • custom_fieldsobject

    Your own per-contact data, as {field_key: value}. Only fields that are set and active appear; unset fields are omitted, never null. Define fields with POST /custom-fields, write values through PATCH /contacts/{id}.

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

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

Account

GET /account

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

Request
curl https://sumgenius.ai/api/v1/account \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200
{
  "data": {
    "account": {
      "id": 1042,
      "business_name": "Riverside Dental",
      "plan": "Professional",
      "addon": "webhook_api"
    },
    "webhooks": {
      "endpoints": [
        {
          "id": 7,
          "name": "Production endpoint",
          "url": "https://api.example.com/webhooks",
          "enabled": true,
          "event_filters": ["message.received"],
          "envelope_style": "standard"
        }
      ]
    }
  }
}

Contacts

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

GET /contacts

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

Query parameters

  • channelenumoptional
  • stageenumoptional
  • tagstringoptional

    Exact tag name; contacts carrying that tag.

  • emailstringoptional

    Exact match.

  • phonestringoptional

    Exact match.

  • namestringoptional

    Substring match on the display name.

  • updated_sincetimestampoptional

    Contacts updated at or after this time.

  • limit · starting_afterpaginationoptional
Request
curl "https://sumgenius.ai/api/v1/contacts?stage=qualified&limit=25" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200 · the contact resource
{
  "data": [
    {
      "id": 19214,
      "channel": "instagram",
      "platform_user_id": "17845629031224067",
      "name": "Riley Chen",
      "email": "riley@example.com",
      "phone": null,
      "ig_username": "rileychen",
      "profile_pic_url": null,
      "stage": "qualified",
      "tags": ["VIP"],
      "lead_score": 30,
      "lead_temperature": "warm",
      "notes": null,
      "custom_fields": { "crm_deal_id": "D-2214" },
      "created_at": "2026-07-25T04:35:23+00:00",
      "updated_at": "2026-07-26T08:05:04+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

GET /contacts/{id}

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

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

POST /contacts

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

Body

  • channelenumrequired
  • platform_user_idstringrequired

    The person's id on that channel, 1 to 255 characters. For SMS this is the phone number.

  • namestringoptional

    Up to 255 characters.

  • emailstringoptional

    Valid email address.

  • phonestringoptional

    Digits and + ( ) . - # x, 3 to 50 characters.

  • stageenumoptional
  • notesstringoptional

    Up to 5,000 characters.

  • tagsstring[]optional

    Tag names; missing tags are created automatically.

  • custom_fieldsobjectoptional

    Merged into the contact's fields, applied atomically with the rest of the upsert. Same semantics as PATCH. Fields must already be defined.

Request
curl -X POST https://sumgenius.ai/api/v1/contacts \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "platform_user_id": "+17025550100",
    "name": "Riley Chen",
    "stage": "new",
    "tags": ["Imported"]
  }'
Response 201 · 200 on update
{
  "data": {
    "id": 19301,
    "channel": "sms",
    "platform_user_id": "+17025550100",
    "name": "Riley Chen",
    "stage": "new",
    "tags": ["Imported"],
    ...
  }
}

PATCH /contacts/{id}

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

Body · all optional

  • stageenum
  • namestring

    Up to 255 characters, or "" to clear.

  • emailstring

    Valid email, or "" to clear.

  • phonestring

    3 to 50 characters, or "" to clear.

  • notesstring

    Up to 5,000 characters, or "" to clear.

  • custom_fieldsobject

    Merge semantics: only the keys you send change, null as a value clears that field, other fields are untouched. Values are validated against each field's type (400 names the offending key). Every actual change fires a contact.field_changed webhook event.

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

POST /contacts/{id}/tags

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

Body

  • namestringrequired

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

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

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

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

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

DELETE /contacts/{id}

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

⚠️

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.

Request
curl -X DELETE https://sumgenius.ai/api/v1/contacts/19214 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200
{
  "data": {
    "id": 19214,
    "object": "contact",
    "deleted": true
  }
}

Custom fields

Your own per-contact data. Define up to 15 fields (text, number, date, or boolean), then write values through PATCH /contacts/{id}. 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.

Request
curl https://sumgenius.ai/api/v1/custom-fields \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200
{
  "data": [
    {
      "id": 3,
      "object": "custom_field",
      "field_key": "crm_deal_id",
      "label": "CRM deal ID",
      "type": "text",
      "archived": false,
      "created_at": "2026-08-01T18:20:11+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

POST /custom-fields

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

Body

  • field_keystringrequired

    1 to 50 characters of a-z 0-9 _. Permanent after create — it is the JSON key in every payload.

  • labelstringrequired

    Display name, 1 to 100 characters. Renameable later.

  • typeenumoptional

    text (default, up to 500 chars) · number · date (YYYY-MM-DD) · boolean. Permanent after create.

Request
curl -X POST https://sumgenius.ai/api/v1/custom-fields \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "field_key": "crm_deal_id",
    "label": "CRM deal ID",
    "type": "text"
  }'
Then set a value
curl -X PATCH https://sumgenius.ai/api/v1/contacts/19214 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "custom_fields": { "crm_deal_id": "D-2214" }
  }'

PATCH /custom-fields/{id}

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

Body

  • labelstringoptional
  • archivedbooleanoptional

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

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

Conversations

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

GET /conversations

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

Query parameters

  • channelenumoptional
  • statusenumoptional
  • assigned_tointegeroptional

    A team member id from team.

  • 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.

Request
curl "https://sumgenius.ai/api/v1/conversations?status=escalated" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200 · the conversation resource
{
  "data": [
    {
      "id": 188892,
      "channel": "instagram",
      "platform_user_id": "17845629031224067",
      "customer_name": "Riley",
      "status": "escalated",
      "assigned_to": 90011,
      "assigned_to_name": "Sam Ortiz",
      "contact_id": 19214,
      "lead_email": "riley@example.com",
      "lead_phone": null,
      "last_message_at": "2026-07-26T08:05:04+00:00",
      "created_at": "2026-07-25T04:35:23+00:00",
      "updated_at": "2026-07-26T13:00:54+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

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.

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

DELETE /conversations/{id}

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

⚠️

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.

Request
curl -X DELETE https://sumgenius.ai/api/v1/conversations/188892 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200
{
  "data": {
    "id": 188892,
    "object": "conversation",
    "deleted": true,
    "removed": {
      "messages": 15,
      "appointments": 1,
      "conversation_tags": 2,
      "contexts": 1,
      "metrics_detached": 5
    }
  }
}

Messages

GET /conversations/{id}/messages

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

  • directionderived

    user is inbound; bot, agent, followup are outbound; the rest is system.

  • typeenum

    text · image · video · audio · file · quick_reply · button · postback · comment_trigger

  • body_purgedboolean

    When a body was purged for compliance, text is null and this is true; the message is still listed.

Request
curl "https://sumgenius.ai/api/v1/conversations/188892/messages" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200 · the message resource
{
  "data": [
    {
      "id": 56698,
      "message_id": "usr_6a65bfb099523",
      "direction": "inbound",
      "sender_type": "user",
      "sender_name": null,
      "type": "text",
      "text": "Do you have anything open Friday?",
      "attachments": [],
      "is_template": false,
      "body_purged": false,
      "created_at": "2026-07-26T08:05:04+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "56610"
}

POST /messages

Send on any channel. Same engine as the Send API with full parity; recommended for new integrations. Route by conversation_id, by platform + platform_user_id, or by to_phone for SMS and WhatsApp.

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

HTTPcodeWhen
400invalid_requestMissing or invalid field
403forbiddenChannel not permitted
404not_foundNo conversation for the routing given
410recipient_opted_outRecipient opted out (SMS STOP)
412not_provisionedChannel not connected
422unprocessable_sendPermanent rejection: unsupported action, expired window, invalid recipient
429rate_limitedToo many sends; retry later
503service_unavailableTemporary failure; may queue for retry
Request
curl -X POST https://sumgenius.ai/api/v1/messages \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "order-4821-confirm",
    "conversation_id": 188892,
    "message_text": "Your order shipped today."
  }'
Response 200
{
  "data": {
    "status": "sent",
    "request_id": "msgreq_c8d83cf861ec1f48",
    "conversation_id": 188892,
    "platform": "instagram",
    "action": "send_message",
    "sent_at": "2026-07-30T06:14:18+00:00",
    "message": "Message sent successfully."
  }
}
Error 422
{
  "error": {
    "code": "unprocessable_send",
    "message": "action 'react' is not supported for platform 'telegram'.",
    "status": "dead_letter"
  }
}

Tags

Tag definitions live account-wide; attaching them to contacts is a contact operation.

GET /tags

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

Request
curl https://sumgenius.ai/api/v1/tags \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200
{
  "data": [
    {
      "id": 14,
      "name": "Booking",
      "color": "#DD6B20",
      "created_at": "2026-03-06T19:55:17+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

POST /tags

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

Body

  • namestringrequired

    1 to 50 characters, unique per account.

  • colorstringoptional

    6-digit hex like #FF0000; anything else is 400.

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

DELETE /tags/{id}

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

Request
curl -X DELETE https://sumgenius.ai/api/v1/tags/14 \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200
{"data": {"id": 14, "deleted": true}}

Appointments

Bookings made through the AI booking agent. 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.

Request
curl "https://sumgenius.ai/api/v1/appointments?status=confirmed" \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200 · the appointment resource
{
  "data": [
    {
      "id": 91772,
      "confirmation_code": "SG-FHZS9V",
      "conversation_id": 187864,
      "contact_id": 16240,
      "channel": "instagram",
      "datetime": "2026-07-24T23:00:00+00:00",
      "timezone": "America/Los_Angeles",
      "duration_minutes": 60,
      "service_type": "Consultation",
      "cost": "0.00",
      "status": "confirmed",
      "meeting_link": "https://meet.google.com/abc-defg-hij",
      "google_event_id": "oa9daqujesg8dktkb777ds5gs0",
      "reminder_sent": false,
      "location": {"city": null, "state": null},
      "customer": {
        "name": "Riley",
        "email": "riley@example.com",
        "phone": "+17025550100",
        "address": null
      },
      "notes": "Prefers mornings",
      "created_at": "2026-07-24T07:33:22+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

GET /appointments/{id}

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

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

Team

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

Request
curl https://sumgenius.ai/api/v1/team \
  -H "X-SumGenius-Api-Key: sgwh_your_api_key_here"
Response 200
{
  "data": [
    {
      "id": 90011,
      "name": "Sam Ortiz",
      "email": "sam@example.com",
      "role": "manager",
      "status": "online",
      "avatar_url": null,
      "active_conversations": 1,
      "total_resolved": 0,
      "created_at": "2026-07-22T06:16:06+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}