Skip to content
Conversation API

Response Formats

Configure response shape per API key — Markdown, plain text, Messenger, WhatsApp, or Instagram.

On this page

Response Formats#

Conversation API responses can be shaped to fit the channel you're delivering them on. Configure the format once per API key in the Console — the same chat agent runs, only the response envelope changes.

Backward compatible. Existing keys default to markdown and continue to receive the same content/products/actions shape they always have. The only additive change is a new top-level format field on every non-error response — strict schema validators should add it to their parser.

Available Formats#

FormatUse whenResponse shape adds
markdownWeb chat UIs, existing integrationsNothing — today's shape (now with format)
plain_textSMS bridges, voice agents, no-UI flowscontent is plain text; links flattened to text (url); images dropped
facebook_messengerMiddleware bridging FB Messenger → your bot → Gydrelements (≤10), quick_replies (≤13), attachments
instagram_messagingInstagram Direct middlewareelements (≤10), quick_replies (≤13), attachments — web_url/postback buttons only, ≤1000-char text
whatsapp_businessWhatsApp Cloud API middlewarebuttons (≤3) OR list (≤10), media_items (≤5), native locations, attachments

Configuring the Format#

  1. Go to Console → Settings → Developer
  2. Create a new API key or edit an existing one — the Response Content Format dropdown unlocks once the conversations:chat scope is enabled
  3. Pick the format and save
  4. Changes take effect immediately — the next call to POST /widgets/{widgetId}/conversations/{sessionId}/messages uses the new format

Response Examples#

Same agent turn, four shapes. All examples assume the agent returned a couple of product suggestions and a handoff quick reply.

{
  "data": {
    "messageId": "msg_01HX...",
    "format": "markdown",
    "content": "Here are two pizzas you might like:\n\n- **Margherita** — RM28\n- **Pepperoni** — RM32\n\nLet me bring in a member of our team to help you with this.",
    "products": [
      { "id": "p1", "name": "Margherita", "price": 28, "currency": "MYR", "image": "https://cdn/..." },
      { "id": "p2", "name": "Pepperoni",  "price": 32, "currency": "MYR", "image": "https://cdn/..." }
    ],
    "actions": [
      { "label": "Talk to staff", "value": "handoff:int_support" }
    ],
    "handoff": true,
    "sessionEnded": false,
    "credits": { "chat": 12, "retrieval": 2, "orchestration": 1, "embedding": 0, "rerank": 1, "plugin": 2, "pluginRerank": 0, "crawler": 0 },
    "usage": { "inputTokens": 482, "outputTokens": 73 }
  }
}

The credits Field#

Every conversation response carries a credits object alongside usage. It has seven numeric keys — chat, retrieval, orchestration, embedding, rerank, plugin, and crawler — and all seven are always present, so you can read any key without a presence check. A key that did not apply to the turn is 0. What each key meters is documented in How Credits Work.

Breaking change (1.31.0)

The credits.api field was renamed to credits.plugin, and three new buckets were added — retrieval, orchestration, and crawler. If your integration reads credits.api, update it to read credits.plugin.

Mixed Knowledge Base Example (Facebook Messenger)#

When the agent surfaces both products and locations in the same turn, they share the 10-element cap. Products come first, then locations. FAQs and document chunks stay inline in content — they don't produce elements.

{
  "data": {
    "messageId": "msg_01HX...",
    "format": "facebook_messenger",
    "content": "Our delivery policy is free over RM50 (FAQ). Two outlets near you are open now.",
    "elements": [
      {
        "title": "Margherita",
        "subtitle": "RM28",
        "image_url": "https://cdn/margherita.jpg",
        "buttons": [{ "type": "web_url", "title": "Order", "url": "https://shop/margherita" }]
      },
      {
        "title": "KL Sentral Outlet",
        "subtitle": "Level 2, Nu Sentral, 50470 KL",
        "image_url": "https://cdn/outlets/kl-sentral.jpg",
        "buttons": [
          { "type": "web_url",      "title": "View on map", "url": "https://maps.google.com/?q=3.1349,101.6869" },
          { "type": "phone_number", "title": "Call",        "payload": "+60312345678" },
          { "type": "web_url",      "title": "Website",     "url": "https://example.com/kl-sentral" }
        ]
      },
      {
        "title": "Bangsar Outlet",
        "subtitle": "Jln Maarof, Bangsar, 59100 KL",
        "image_url": "https://cdn/outlets/bangsar.jpg",
        "buttons": [
          { "type": "web_url",      "title": "View on map", "url": "https://maps.google.com/?q=3.1262,101.6724" },
          { "type": "phone_number", "title": "Call",        "payload": "+60312345679" }
        ]
      }
    ],
    "products":  [ /* ... */ ],
    "locations": [ /* still present with full lat/lng */ ],
    "actions":   [],
    "handoff":   false,
    "sessionEnded": false,
    "credits":   { /* ... */ },
    "usage":     { /* ... */ }
  }
}

Action Protocol (handoff & end-session)#

Quick reply / button payloads round-trip the raw action.value from the agent. Two prefixes are signals your middleware reads; everything else is plain text:

  • handoff:<integrationId> — a human team is ready for live chat. The id is the team's integration id in your account (for example handoff:int_support). It is a signal: the Conversation API never starts a live handoff itself, so your code begins the live chat on your side. It appears only after Gydr confirmed the team can be reached, and only when a single team is reachable.
  • handoff:prompt — several teams are reachable and the visitor must choose. Channels that render actions use it as the cue to ask "which team?". On the Conversation API you will instead receive the team pick as the response's form (with humanAgentStep: "team_pick" and handoff: false); see the section below.
  • end_session:<reason> — an offer to end the session (e.g. end_session:manual). Ending is your call: it is a button for your UI, not something Gydr does on its own. To end, call the end endpoint.
  • Any other value — sent as a plain user message

Your code owns these values. Never post one back as a message: a live handoff starts on your side, handoff steps are answered through the form-submissions endpoint below, and a session ends through the end endpoint. Sending handoff:..., end_session:... or a handoff form's option value (#HANDOFF#..., #HUMANAGENT#...) as the message content returns 400 protocol_value_not_a_message on the content field. No credit is used.

humanAgentStep and server-built forms#

When a visitor asks for a person, shows frustration, or the bot fails repeatedly, the response carries a form that Gydr built itself (not the model) as its single form, and repeats its step at the top level as humanAgentStep so you can branch without reading the form: offer, team_pick or ticket_form. A single reachable Live desk is not a form: it arrives as handoff: true with a handoff:<id> action. A form has the same shape as every other form and is answered the same way, through POST .../form-submissions; the answer is decided by Gydr with no model turn. Example, a team pick:

{
  "data": {
    "format": "markdown",
    "content": "Which team would you like to reach?",
    "handoff": false,
    "humanAgentStep": "team_pick",
    "form": {
      "formId": "df_3f9a1c0b7d2e4a65b8c01",
      "origin": "human_agent_choice",
      "humanAgentStep": "team_pick",
      "cancellable": true,
      "questions": [
        {
          "key": "team_pick",
          "kind": "choice",
          "prompt": "Which team would you like to reach?",
          "required": true,
          "multiSelect": false,
          "options": [
            { "value": "#HANDOFF#int_billing", "label": "Billing" },
            { "value": "#HANDOFF#int_sales", "label": "Sales" }
          ]
        }
      ]
    }
  }
}

Submit the chosen option's value under the question's key. The step table, the live-chat signal, tickets and their credit cost are documented under Sessions & Messages. The same fields appear in every output format; the channel-specific envelopes (quick replies, buttons) carry only the actions.

WhatsApp 24-Hour Customer Service Window#

Your middleware's responsibility. WhatsApp Cloud API only permits free-form responses within 24 hours of the user's last inbound message. Outside that window you must use a pre-approved template. Track the inbound timestamp at your bridge and gate outbound delivery accordingly — Gydr returns the same response shape regardless.

Channel Mapping Reference#

Gydr fieldFB MessengerWhatsApp Cloud API
contentText message body (≤2000 chars, chunked as string[] if longer)Text body in WA inline syntax (≤4096 chars, chunked similarly)
elementsGeneric Template carousel elements array (≤10; ≤3 buttons each)Not directly mapped — use media_items instead
quick_repliesquick_replies array (≤13)Not present — use buttons or list
buttonsNot present — use quick_replies or per-element buttonsInteractive reply-buttons (1–3)
listNot present — promote to multi-element or multiple repliesInteractive list message (≤10 rows; auto-promoted when actions > 3)
media_itemsMap to per-card image + buttonsSend each as image + caption + CTA URL button (≤5)
locationsRender as a card with map link + phone buttonNative location message type
attachmentsImage attachment messagesMedia messages (image / video / document)

Instagram Messaging uses the same elements / quick_replies shape as Facebook, with two differences: buttons are web_url/postback only (no phone/Call button — a location's phone is folded into the element subtitle), and the text body is capped at 1000 characters.