ChatGenius Developer Events Markdown

Webhook Events

Every event ChatGenius can deliver to your endpoint: the shared envelope, the attachment shape, and all 31 event types with full-profile JSON examples.

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

The envelope

Every event is posted as JSON with Content-Type: application/json, in one of two styles. Standard, which every endpoint created since July 2026 receives, wraps the event body: {"type", "timestamp", "data"}, and you read the fields from data.*. Legacy, for integrations created before July 2026, posts the flat body with no wrapper. Everything inside is identical between the two; the style is fixed per endpoint and shown on the endpoint in the portal.

The event type also rides in the X-SumGenius-Event request header on every delivery, and every delivery is signed: see verifying signatures. How much customer data is included is controlled by your payload profile (full or minimal).

Event body fields

  • event_idstring

    32-character hex id, unique per event and stable across retries; it is also the delivery id header. Use it to deduplicate.

  • event_typestring

    One of the 31 types in the index.

  • occurred_atUTC ISO-8601

    When the event happened on the platform.

  • source.platformenum

    facebook · instagram · sms · whatsapp · telegram

  • source.channelenum

    meta_dm · twilio_sms · whatsapp_cloud · telegram

  • conversation.idinteger | null

    ChatGenius conversation id; pass it to the Send API to reply. Can be null on the very first inbound message from a new person.

  • conversation.assigned_to / assigned_to_namefull profile only

    Team member id and name the conversation is assigned to, or null.

  • customer.platform_user_idstring

    Platform-assigned user id. Always present, on both profiles: your stable join key.

  • customer enrichmentfull profile only

    platform_user_name, platform_user_username, contact_id, tags, stage, custom_fields. custom_fields carries the contact's set, active custom field values as {field_key: value}, {} when none are set.

  • customer follow statusfull profile · Meta DM events

    is_follower (do they follow you), is_following (do you follow them), follower_count. Instagram only, null means unknown (never synced, or a Facebook user). From the cached profile sync, except on comment.received where is_follower is checked live at comment time, the gate a follow-gate automation needs before deciding what to reply.

  • messageobject

    {id, text_raw, attachments[]}. Non-message events replace this block with their own (comment, appointment, lead, escalation, send_failed).

  • metaobject

    version (currently v1), payload_profile, generated_at.

Standard envelope · message.received
{
  "type":      "message.received",
  "timestamp": "2026-02-20T08:15:03Z",
  "data": {
    "event_id":    "a3f8e1c2d4b567890abcdef012345678",
    "event_type":  "message.received",
    "occurred_at": "2026-02-20T08:15:00Z",
    "source": {
      "platform": "instagram",
      "channel":  "meta_dm"
    },
    "conversation": {
      "id":               10482,
      "assigned_to":      90011,
      "assigned_to_name": "Anna"
    },
    "customer": {
      "platform_user_id":       "17841400123456789",
      "platform_user_name":     "Jane Doe",
      "platform_user_username": "janedoe",
      "contact_id":    555,
      "tags":          ["VIP", "Returning"],
      "stage":         "qualified",
      "custom_fields": { "crm_deal_id": "D-2214" }
    },
    "message": {
      "id":          "mid.1234567890abcdef",
      "text_raw":    "Hi, do you have availability next Tuesday?",
      "attachments": []
    },
    "meta": {
      "version":         "v1",
      "payload_profile": "full",
      "generated_at":   "2026-02-20T08:15:00Z"
    }
  }
}
Legacy (flat) equivalent
{
  "event_id":    "a3f8e1c2d4b567890abcdef012345678",
  "event_type":  "message.received",
  "occurred_at": "2026-02-20T08:15:00Z",
  "source": {
    "platform": "instagram",
    "channel":  "meta_dm"
  },
  ...
}

Attachments

Media the customer sent arrives in message.attachments. Four fields are always present; the rest ride on the full profile only.

  • typestring

    Attachment category, e.g. image · audio · video.

  • media_typestring

    MIME type when available.

  • media_idstring

    Platform media identifier.

  • received_atUTC ISO-8601

    When the attachment was received.

  • urlfull profile only

    Media URL. Time-limited, provided by the platform; download promptly if you need to keep it.

  • title / captionfull profile only

    Title or caption text when present.

Attachment object
{
  "type":        "image",
  "media_type":  "image/jpeg",
  "media_id":    "18025531234567890",
  "received_at": "2026-02-20T08:15:00Z",

  // full profile only:
  "url":     "https://lookaside.fbsbx.com/ig_messaging_cdn/...",
  "title":   null,
  "caption": null
}

Message events

Inbound DMs and comments on Facebook and Instagram.

message.received

A customer sent a direct message on Facebook or Instagram (source.platform is facebook or instagram). Fires on text and on attachments.

Share variants

message.reel_shared and message.post_shared fire in addition to message.received for the same message when an Instagram reel or post is shared into the thread, carrying the same message.id. Subscribe to them when you want to react to shares specifically; deduplicate against message.id if you consume both.

message.received · full profile
{
  "event_id":    "a3f8e1c2d4b567890abcdef012345678",
  "event_type":  "message.received",
  "occurred_at": "2026-02-20T08:15:00Z",
  "source": {
    "platform": "instagram",
    "channel":  "meta_dm"
  },
  "conversation": {
    "id":               10482,
    "assigned_to":      90011,
    "assigned_to_name": "Anna"
  },
  "customer": {
    "platform_user_id":       "17841400123456789",
    "platform_user_name":     "Jane Doe",
    "platform_user_username": "janedoe",
    "contact_id":    555,
    "tags":          ["VIP", "Returning"],
    "stage":         "qualified",
    "custom_fields": { "crm_deal_id": "D-2214" }
  },
  "message": {
    "id":          "mid.1234567890abcdef",
    "text_raw":    "Hi, do you have availability next Tuesday?",
    "attachments": []
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-02-20T08:15:00Z"
  }
}

comment.received

A customer left a comment on one of your Instagram or Facebook posts, reels, or live videos. Fires for every inbound comment, whether or not a ChatGenius comment trigger is configured.

There is no message block; the payload carries source_post and comment instead. conversation.id is null for comments: the follow-up DM, if a trigger sends one, arrives as its own message.received event.

  • source_postobject

    id is always present: the attribution join key. url is the permalink, populated when the comment matched a trigger targeting a specific post, otherwise null.

  • commentobject

    {id, text_raw, matched_keyword, trigger_id, trigger_name, source_type}. source_type is comment · live. The trigger fields (matched_keyword, trigger_id, trigger_name) are null unless a comment trigger matched.

comment.received · trigger matched
{
  "event_id":    "b7c2e1a4f9d063518adef0129876dcba",
  "event_type":  "comment.received",
  "occurred_at": "2026-02-20T08:15:00Z",
  "source": {
    "platform": "instagram",
    "channel":  "meta_dm"
  },
  "conversation": {
    "id": null
  },
  "customer": {
    "platform_user_id":       "17841400123456789",
    "platform_user_name":     null,
    "platform_user_username": "janedoe",
    "is_follower":            true,
    "is_following":           null,
    "follower_count":         null
  },
  "source_post": {
    "id":  "17912345678901234",
    "url": "https://www.instagram.com/p/CtExAmple/"
  },
  "comment": {
    "id":              "17987654321098765",
    "text_raw":        "PRICE",
    "matched_keyword": "price",
    "trigger_id":      42,
    "trigger_name":    "Pricing DM",
    "source_type":     "comment"
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-02-20T08:15:00Z"
  }
}

comment.trigger_fired

Fires after a comment trigger has acted on a comment. Where comment.received fires for every comment and only predicts the matching trigger, this fires once per trigger fire, after the work is done, and says what was actually sent. Use it to count real conversions per trigger or to hand the commenter to your CRM.

  • triggerobject

    {id, name, matched_keyword}. The trigger row and the keyword that matched.

  • actionsobject

    {public_reply_sent, dm_sent, flow_id, fired_at}. Booleans for what went out; flow_id is the linked flow that started, or null.

  • source_post · commentobject

    Same shape as comment.received: the post (id, url) and the comment (id, text_raw, source_type).

No conversation.id: a comment is not a DM thread. The DM the trigger sent opens or continues one, and customer.platform_user_id is how you join to it.

comment.trigger_fired
{
  "type": "comment.trigger_fired",
  "timestamp": "2026-09-05T18:20:00Z",
  "data": {
    "event_type": "comment.trigger_fired",
    "platform": "instagram",
    "conversation": { "id": null },
    "customer": { "platform_user_id": "17841400123456789", "platform_user_username": "janedoe" },
    "source_post": { "id": "17912345678901234", "url": "https://www.instagram.com/p/example/" },
    "comment": { "id": "17987654321098765", "text_raw": "Can I get the guide?", "source_type": "comment" },
    "trigger": { "id": 212, "name": "Guide", "matched_keyword": "guide" },
    "actions": { "public_reply_sent": true, "dm_sent": true, "flow_id": null, "fired_at": "2026-09-05T18:20:00Z" },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-05T18:20:00Z" }
  }
}

comment.handled

Fires each time the AI Comment Manager changes the state of a comment no trigger claimed: a reply was queued for approval, posted, hidden by moderation, approved or rejected by a person, failed, or skipped at the plan cap. One delivery per state change, so a comment that is queued and then approved fires twice.

  • handledobject

    The log row as GET /comments/{id} returns it: {id, status, sentiment, moderation, reply, reviewed_by, reviewed_at}. status is what changed. handled.id is what you pass to approve or reject.

  • source_post · commentobject

    The post id, and the comment (id, text_raw). source_post.url is null here.

Subscribe to this with handled.status = pending to get an approval queue pushed to Slack or a help desk, then approve from there.

comment.handled
{
  "type": "comment.handled",
  "timestamp": "2026-09-05T18:40:02Z",
  "data": {
    "event_type": "comment.handled",
    "platform": "instagram",
    "conversation": { "id": null },
    "customer": { "platform_user_id": "17841400123456789", "platform_user_username": "janedoe" },
    "source_post": { "id": "17912345678901234", "url": null },
    "comment": { "id": "17987654321098765", "text_raw": "How much is this?", "source_type": "comment" },
    "handled": {
      "id": 57,
      "status": "pending",
      "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.", "sent_text": null, "edited": false, "sent_at": null, "meta_reply_id": null },
      "reviewed_by": null,
      "reviewed_at": null
    },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-05T18:40:02Z" }
  }
}

message.postback

A customer tapped a button in an Instagram or Facebook DM. Fires for every postback tap on the account, whatever created the button, a flow, a comment trigger DM, or a button you sent through the Send API. For API senders this is the round trip: the postback.payload is the payload you set when you sent the button.

Same envelope as message.received, the message block carries the button's title as text_raw, plus a postback block:

  • postbackobject

    {payload, title}. payload is the machine value the button was created with; title is what the customer saw (may be null).

ℹ️

The tap opens the 24h messaging window. After this event you can respond with regular send_message calls, no private reply needed anymore.

message.postback
{
  "event_type": "message.postback",
  "source": { "platform": "instagram", "channel": "instagram" },
  "conversation": { "id": 10482 },
  "customer": {
    "platform_user_id": "1869400170313836",
    "username": "jaredr.media"
  },
  "message": {
    "id": "aWdf...",
    "text_raw": "Send me more",
    "attachments": []
  },
  "postback": {
    "payload": "more_guides",
    "title": "Send me more"
  }
}

sms · whatsapp · telegram .message_received

A customer messaged you on SMS, WhatsApp, or Telegram. Same envelope as every other event, plus one channel sub-block under message. The channel pages cover routing identifiers, supported actions, and limits.

  • message.smsobject

    {to_phone, from_phone, num_segments, num_media, encoding}. encoding is GSM-7 · UCS-2.

  • message.whatsappobject

    {wa_id, phone_number, business_phone_number_id, profile_name, message_type, context_message_id}.

  • message.telegramobject

    {chat_id, chat_type, username, message_type}. chat_id is the recipient identifier for the Send API.

sms.message_received · full profile
{
  "event_id":    "7f7a80c84bf17c4b5b72974c97365a77",
  "event_type":  "sms.message_received",
  "occurred_at": "2026-04-27T23:22:10Z",
  "source": {
    "platform": "sms",
    "channel":  "twilio_sms"
  },
  "conversation": {
    "id": 119301
  },
  "customer": {
    "platform_user_id": "+15551234567",
    "phone":            "+15551234567"
  },
  "message": {
    "id":          "SMdb4342728f73bca6a83770a705e27b31",
    "text_raw":    "Hi, what time do you open Saturday?",
    "attachments": [],
    "sms": {
      "to_phone":     "+15559876543",
      "from_phone":   "+15551234567",
      "num_segments": 1,
      "num_media":    0,
      "encoding":     "GSM-7"
    }
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-04-27T23:22:10Z"
  }
}
Channel sub-blocks
// sms.message_received
"sms": {
  "to_phone":     "+15559876543",
  "from_phone":   "+15551234567",
  "num_segments": 1,
  "num_media":    0,
  "encoding":     "GSM-7"
}

// whatsapp.message_received
"whatsapp": {
  "wa_id":                    "15551234567",
  "phone_number":             "+15551234567",
  "business_phone_number_id": "108765432109876",
  "profile_name":             "Jane Doe",
  "message_type":             "text",
  "context_message_id":       null
}

// telegram.message_received
"telegram": {
  "chat_id":      "820032950394075",
  "chat_type":    "private",
  "username":     "janedoe",
  "message_type": "text"
}

Lead events

Contact info captured from conversation content, ready for your CRM.

lead.email_captured · lead.phone_captured

Fires once per conversation, the first time an email (or phone) is captured from the conversation content, on any channel. A conversation that captures an email and later a phone produces one of each; the two events are identical except for event_type.

The lead block {email, phone, captured_at} is always included regardless of payload profile: contact info is the point of the event. Fields not yet captured are null.

lead.phone is a phone collected from message content, not the SMS or WhatsApp sender number; on those channels the sender number is already delivered as customer.platform_user_id on every event.

lead.email_captured
{
  "event_id":    "c7a3b5d1e9f2468013579bdf2468ace0",
  "event_type":  "lead.email_captured",
  "occurred_at": "2026-07-23T07:20:00Z",
  "source": {
    "platform": "instagram",
    "channel":  "meta_dm"
  },
  "conversation": {
    "id": 119789
  },
  "customer": {
    "platform_user_id": "992584566622479"
  },
  "lead": {
    "email":       "[email protected]",
    "phone":       "+15551234567",
    "captured_at": "2026-07-23T07:20:00Z"
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-07-23T07:20:00Z"
  }
}

Appointment events

In-chat bookings made through the AI booking agent. Calendar syncs do not fire these.

appointment.booked · .cancelled · .rescheduled

All three carry the same appointment block; the event_type and the appointment's status tell you what happened. appointment.rescheduled fires again on each subsequent reschedule, with the new datetime. The block is always full regardless of payload profile.

  • appointmentobject

    {id, confirmation_code, datetime, service_type, duration_minutes, cost, status, timezone, location {city, state}, customer {name, email, phone}}.

  • datetimeUTC ISO-8601

    Always UTC. timezone is the appointment's own booking timezone; convert back to display the time the customer chose.

  • appointment.customerobject

    The contact details captured during booking. The top-level customer remains the messaging identity.

appointment.booked
{
  "event_id":    "a1b2c3d4e5f60718293a4b5c6d7e8f90",
  "event_type":  "appointment.booked",
  "occurred_at": "2026-07-24T06:30:00Z",
  "source": {
    "platform": "instagram",
    "channel":  "meta_dm"
  },
  "conversation": {
    "id": 187765
  },
  "customer": {
    "platform_user_id":   "820032950394075",
    "platform_user_name": "Jared"
  },
  "appointment": {
    "id":                1234,
    "confirmation_code": "AB12CD",
    "datetime":          "2026-07-30T15:00:00Z",
    "service_type":      "consultation",
    "duration_minutes":  30,
    "cost":              null,
    "status":            "confirmed",
    "timezone":          "America/New_York",
    "location": {
      "city":  "Austin",
      "state": "TX"
    },
    "customer": {
      "name":  "Jane Doe",
      "email": "[email protected]",
      "phone": "+15551234567"
    }
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-07-24T06:30:00Z"
  }
}

Conversation events

Human handoff.

conversation.escalated

Fires every time a conversation is handed to a human, triggered by negative sentiment or an explicit hand-off, including repeat escalations on the same conversation. Wire it to Slack or a ticketing system, and match it to a thread via conversation.id.

There is no de-escalation event: the AI stays paused on an escalated conversation by design.

  • escalationobject

    {reason, assigned_to, escalated_at}. assigned_to is the agent the conversation was routed to, or null if no agent was available.

conversation.escalated
{
  "event_id":    "9d4f7a3b1c2e5d6f8091a2b3c4d5e6f7",
  "event_type":  "conversation.escalated",
  "occurred_at": "2026-07-24T05:40:00Z",
  "source": {
    "platform": "instagram",
    "channel":  "meta_dm"
  },
  "conversation": {
    "id": 187765
  },
  "customer": {
    "platform_user_id":       "820032950394075",
    "platform_user_name":     "Jared",
    "platform_user_username": "jaredr.media"
  },
  "escalation": {
    "reason":       "Negative sentiment detected",
    "assigned_to":  "Alex",
    "escalated_at": "2026-07-24T05:40:00Z"
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-07-24T05:40:00Z"
  }
}

conversation.resolved

Fires when a conversation is closed, from the inbox or through PATCH /conversations/{id}. Resolving hands the thread back to the AI, so this is also the moment any assignment ends. One delivery per resolution; a thread resolved, reopened and resolved again fires twice.

  • resolutionobject

    {resolved_at, resolved_by, user}. resolved_by is manual for the inbox, api for the API, or another internal reason. user is the person or API key that did it.

conversation.resolved
{
  "type": "conversation.resolved",
  "timestamp": "2026-09-05T02:10:00Z",
  "data": {
    "event_type": "conversation.resolved",
    "platform": "instagram",
    "conversation": { "id": 188892, "status": "resolved" },
    "customer": { "platform_user_id": "820032950394075", "platform_user_name": "Jared", "contact_id": 19214 },
    "resolution": {
      "resolved_at": "2026-09-05T02:10:00Z",
      "resolved_by": "api",
      "user": "API key 6"
    },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-05T02:10:00Z" }
  }
}

conversation.assigned

Fires when who holds a conversation changes: assigned to a team member, claimed by the account owner, or handed back to the AI. Every change fires, so a consumer can mirror the inbox without polling. Escalations that auto-assign an agent still fire conversation.escalated, not this.

  • assignmentobject

    {assigned_to, assigned_to_name, kind, source, assigned_at}. kind is team (a positive team member id), owner (the account owner, negative id), or none (handed back to the AI, assigned_to null). source is portal, api, or the status reason that cleared it, such as manual on a resolve.

conversation.assigned
{
  "type": "conversation.assigned",
  "timestamp": "2026-09-05T02:10:00Z",
  "data": {
    "event_type": "conversation.assigned",
    "platform": "instagram",
    "conversation": { "id": 188892, "status": "active" },
    "customer": { "platform_user_id": "820032950394075", "platform_user_name": "Jared", "contact_id": 19214 },
    "assignment": {
      "assigned_to": -50,
      "assigned_to_name": "API key 6",
      "kind": "owner",
      "source": "api",
      "assigned_at": "2026-09-05T02:10:00Z"
    },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-05T02:10:00Z" }
  }
}

Contact events

CRM data changes.

contact.tag_added · contact.tag_removed

Fires when a tag is put on or taken off a contact. Every path fires it: a person in the inbox or the contact card, the API, a flow's tag action, and automatic source tagging. Adding a tag the contact already has, or removing one it does not have, fires nothing.

  • tagobject

    {id, name, color, source, changed_at}. source is portal, api, or automatic (a flow or source tagging).

No message object. customer.contact_id and customer.tags carry the contact and its full tag list after the change.

contact.tag_added
{
  "type": "contact.tag_added",
  "timestamp": "2026-09-05T20:00:00Z",
  "data": {
    "event_type": "contact.tag_added",
    "platform": "instagram",
    "conversation": { "id": 188892 },
    "customer": { "platform_user_id": "17841400123456789", "platform_user_username": "janedoe", "contact_id": 19214, "tags": ["VIP", "Hot lead"] },
    "tag": { "id": 31, "name": "Hot lead", "color": "#EF4444", "source": "api", "changed_at": "2026-09-05T20:00:00Z" },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-05T20:00:00Z" }
  }
}

contact.stage_changed

Fires when a contact moves to a different pipeline stage, from the contact card or PATCH /contacts/{id}. Setting the stage it already has fires nothing.

  • stageobject

    {old, new, source, changed_at}. Stages are new, contacted, qualified, customer, lost. source is portal or api.

contact.stage_changed
{
  "type": "contact.stage_changed",
  "timestamp": "2026-09-05T20:01:00Z",
  "data": {
    "event_type": "contact.stage_changed",
    "platform": "instagram",
    "conversation": { "id": 188892 },
    "customer": { "platform_user_id": "17841400123456789", "contact_id": 19214, "stage": "qualified" },
    "stage": { "old": "contacted", "new": "qualified", "source": "portal", "changed_at": "2026-09-05T20:01:00Z" },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-05T20:01:00Z" }
  }
}

contact.updated

Fires when a contact's name, email or phone is edited, in the portal or via the API. One event per save, listing every field that actually changed. Custom fields have their own event, contact.field_changed; captures made by the AI in chat fire lead.*_captured instead.

  • updatedobject

    {fields, source, changed_at, values}. fields is any of display_name, email, phone. values is {field: {old, new}} and rides on the full payload profile only, since email and phone are personal data; minimal-profile destinations get the field names and nothing else.

contact.updated
{
  "type": "contact.updated",
  "timestamp": "2026-09-05T20:02:00Z",
  "data": {
    "event_type": "contact.updated",
    "platform": "instagram",
    "conversation": { "id": 188892 },
    "customer": { "platform_user_id": "17841400123456789", "contact_id": 19214 },
    "updated": {
      "fields": ["email"],
      "source": "api",
      "changed_at": "2026-09-05T20:02:00Z",
      "values": { "email": { "old": null, "new": "[email protected]" } }
    },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-05T20:02:00Z" }
  }
}

contact.field_changed

Fires when a custom field value changes on a contact, whether the change came from the portal or the API. One event per changed field. Wire it to your CRM to mirror ChatGenius data without polling.

Delivery is best-effort at-least-once like every event: treat GET /v1/contacts as the reconciliation source of truth.

  • field_changedobject

    {field_key, label, version, changed_at, source, old_value, new_value}. source is portal, api, or flow (captured by a flow's Collect Input or Set Custom Field action).

  • versioninteger

    A per-contact counter that increments on every field write. Keep the highest seen per contact and ignore lower ones, retries can deliver out of order, and the version tells you which event is stale. It is not a delivery-order guarantee.

  • old_value · new_valuefull profile only

    Omitted on the minimal payload profile, field values can carry sensitive data, so minimal-profile destinations get the fact of the change without the values.

  • conversation.idnullable

    null for a contact with no conversation (for example one created via POST /v1/contacts).

contact.field_changed
{
  "event_id":    "c3a2b1d0e9f8a7b6c5d4e3f2a1b0c9d8",
  "event_type":  "contact.field_changed",
  "occurred_at": "2026-08-01T18:22:40Z",
  "source": {
    "platform": "instagram",
    "channel":  "meta_dm"
  },
  "conversation": {
    "id": 187765
  },
  "customer": {
    "platform_user_id":       "820032950394075",
    "platform_user_name":     "Maya R.",
    "platform_user_username": "maya.r",
    "contact_id": 128,
    "tags": ["Lead"],
    "stage": "qualified",
    "custom_fields": { "refill_due": "2026-08-14" }
  },
  "field_changed": {
    "field_key":  "refill_due",
    "label":      "Refill due",
    "version":    7,
    "changed_at": "2026-08-01T18:22:40Z",
    "source":     "portal",
    "old_value":  "2026-07-14",
    "new_value":  "2026-08-14"
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-08-01T18:22:40Z"
  }
}

post.scheduled · post.published · post.failed

A Content Studio post changed state. post.scheduled fires when a post is scheduled or its time is moved, from the portal or the API. post.published fires when the post is live on every target, whether it was published now or by the scheduler. post.failed fires when a platform rejected it; post.error_message says why and POST /posts/{id}/retry tries again.

  • postobject

    The post as GET /posts/{id} returns it: status, targets, caption, media, published_ids per platform, error_message, retry_count.

These events have no customer and no conversation: a post is not about a person. customer is null and conversation.id is null.

post.published
{
  "type": "post.published",
  "timestamp": "2026-09-12T16:00:07Z",
  "data": {
    "event_type": "post.published",
    "platform": "instagram",
    "conversation": { "id": null },
    "customer": null,
    "post": {
      "id": 90050, "status": "published", "platform": "instagram", "targets": ["instagram"], "post_type": "reels",
      "caption": "Behind the scenes of the new setup.", "published_at": "2026-09-12T16:00:07+00:00",
      "published_ids": { "instagram_media_id": "17912345678901234", "facebook_post_id": null, "tiktok_video_id": null, "tiktok_publish_id": null },
      "error_message": null, "retry_count": 0
    },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-12T16:00:07Z" }
  }
}

Flow events

A flow changed state, or a person finished one.

flow.published · flow.activated · flow.paused · flow.session_completed

flow.published fires when a draft becomes the live definition, from the builder or POST /flows/{id}/publish; flow.published_version is the new version. flow.activated fires when the flow goes live and flow.paused when it stops; pausing also cancels the sessions that were open. These three carry a flow block and no person: customer is null and conversation.id is null.

flow.session_completed is about a person: it fires when someone reaches the end of a flow (end_reason: reached_end) or the flow hands them to the AI (ai_handoff). It carries the usual customer and conversation plus a flow_session block; the session id matches GET /flow-sessions/{id}, which has the path they took. Someone who stops answering does not complete, so this is the count of people who got through, not the count who entered.

  • flowobject

    {id, name, status, trigger_source, keywords, channels, version, published_version}. Published events add went_live.

  • flow_sessionobject

    {id, flow_id, flow_name, flow_version, end_reason, started_at, completed_at}.

flow.session_completed
{
  "type": "flow.session_completed",
  "timestamp": "2026-09-14T18:22:41Z",
  "data": {
    "event_type": "flow.session_completed",
    "platform": "instagram",
    "conversation": { "id": 4940 },
    "customer": { "platform_user_id": "820032950394075", "platform_user_username": "maya.r", "contact_id": 128 },
    "flow_session": {
      "id": 7731, "flow_id": 184, "flow_name": "Guide request", "flow_version": 2,
      "end_reason": "reached_end",
      "started_at": "2026-09-14T18:20:03+00:00", "completed_at": "2026-09-14T18:22:41+00:00"
    },
    "meta": { "version": "v1", "payload_profile": "full", "generated_at": "2026-09-14T18:22:41Z" }
  }
}

Delivery events

Outbound sends that permanently failed.

message.send_failed

Fires when an outbound Send API request permanently fails: the platform returned a non-retryable error (most commonly an expired Meta messaging window) or retries were exhausted. Your system believed the send succeeded; this event lets it reconcile. Match it back to your original request via send_failed.idempotency_key.

  • send_failedobject

    {action, idempotency_key, request_id, error_code, error_message, http_code, attempts, text_attempted}.

  • error_codeSMS

    For SMS this is the Twilio error code. 30006 (landline or a carrier that cannot receive SMS) reaches you two ways: from the carrier after the send, and from us before it, once a carrier has already rejected that number and we refuse to pay for the same rejection again. Same code either way, so handle it once.

  • text_attemptedfull profile only

    The message text you tried to send. null on the minimal profile.

message.send_failed
{
  "event_id":    "b2f1e0d9c8b7a695847362514f0e1d2c",
  "event_type":  "message.send_failed",
  "occurred_at": "2026-07-23T06:40:00Z",
  "source": {
    "platform": "instagram",
    "channel":  "meta_dm"
  },
  "conversation": {
    "id": 42
  },
  "customer": {
    "platform_user_id": "178414"
  },
  "send_failed": {
    "action":          "send_message",
    "idempotency_key": "your-key-abc-123",
    "request_id":      "req-xyz-789",
    "error_code":      "10",
    "error_message":   "Meta 7-day window expired.",
    "http_code":       400,
    "attempts":        1,
    "text_attempted":  "Hi, following up on your order"
  },
  "meta": {
    "version":         "v1",
    "payload_profile": "full",
    "generated_at":   "2026-07-23T06:40:00Z"
  }
}