Skip to content
Conversation API

Sessions & Messages

Create AI chat sessions, send messages, submit forms, and receive responses via REST or callbacks.

On this page

Conversations API#

The Conversations API lets you create and manage AI chat sessions programmatically. Use it to integrate Gydr-powered chat into social media platforms, custom chat interfaces, mobile apps, messaging services, and any channel beyond the widget SDK.

Endpoints#

MethodPath
POST/widgets/{widgetId}/conversations
POST/widgets/{widgetId}/conversations/{sessionId}/messages
POST/widgets/{widgetId}/conversations/{sessionId}/end
POST/widgets/{widgetId}/conversations/{sessionId}/rate
GET/widgets/{widgetId}/conversations/{sessionId}
GET/widgets/{widgetId}/conversations/{sessionId}/messages
POST/widgets/{widgetId}/conversations/{sessionId}/form-submissions

Session Lifecycle#

A conversation follows a simple lifecycle:

  1. Create Session — initialize a new chat session for a visitor
  2. Send Messages — exchange messages back and forth with the AI
  3. End Session — resolve the session when the conversation is complete
  4. Rate Session (optional) — collect visitor feedback after the session ends

You can retrieve session details and message history at any point using the GET endpoints.

New: the assistant response shape can now be configured per API key — Markdown (default), Plain text, Facebook Messenger, or WhatsApp Business. See Response Formats for the full reference, code samples, and channel mapping tables.

Response Modes#

The Send Message endpoint supports two response types:

TypeBehaviorBest For
syncSynchronous. Waits for the AI response before returning (~25s timeout).Simple queries, real-time chat UIs
callbackAsynchronous. Returns 202 immediately, delivers the AI response to your callback URL.Complex multi-tool queries, social media integrations

Create Session#

Initializes a new chat session for a given widget. You can optionally provide a custom session ID, a User ID for a signed-in person, visitor details, and a preferred language. Gydr makes the visitor id and returns it.

curl -X POST https://api.gydr.ai/widgets/{widgetId}/conversations \
  -H "X-API-Key: api_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "42",
    "visitorDetails": {
      "name": "John",
      "email": "john@example.com"
    },
    "language": "en",
    "timezone": "Asia/Kuala_Lumpur"
  }'

Response (201 Created for a new session):

{
  "data": {
    "sessionId": "sess_dBLPP9tYcVKrzrxC5_gTR",
    "widgetId": "6751a3e...",
    "visitorId": "usr_3f9c1a7e5b2d48a6b0c4e8f21d7a9b35",
    "userId": "42",
    "status": "active",
    "createdAt": "2026-03-23T10:00:00.000Z",
    "resumed": false
  },
  "meta": { "creditsConsumed": 1 }
}

If you supply a sessionId that already exists and is still active, the API returns 200 with resumed: true instead of 201 — no new session is created and the existing conversation history is preserved.

Create Session Fields#

FieldTypeRequiredDescription
visitorIdstringNoOmit it to start a new visitor: Gydr makes the id and returns it. Pass back only an id Gydr returned from this API for this widget, to start another conversation as the same visitor. Any other value is refused.
userIdstringNoYour own id for a signed-in person (1–255 printable characters). Needs the user:assert scope (else 403) and a sign-in source on the widget (else 400 no_sign_in_source). See Signed-in users.
sessionIdstringNoCustom session ID. Server generates one if omitted.
visitorDetailsobjectNoUnverified details: name, email, phone, company, location (latitude and longitude, optional name)
languagestringNoBCP 47 language code (e.g., "en", "ms", "zh"). AI responds in this language initially.
timezonestringNoIANA timezone (e.g., "Asia/Kuala_Lumpur", "America/New_York"). Used for time-aware responses.

Send Message (Sync)#

Sends a message to the AI and waits for the response. The default response type is sync.

curl -X POST https://api.gydr.ai/widgets/{widgetId}/conversations/{sessionId}/messages \
  -H "X-API-Key: api_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "What products do you have under $50?",
    "response_type": "sync"
  }'

Response:

{
  "data": {
    "messageId": "msg_abc123",
    "content": "Here are some products under $50...",
    "products": [
      { "id": "prod_1", "name": "Wireless Earbuds", "price": 29.99 }
    ],
    "actions": [],
    "sessionEnded": false,
    "credits": { "chat": 8, "retrieval": 2, "orchestration": 1, "embedding": 0, "rerank": 1, "plugin": 1, "pluginRerank": 0, "crawler": 0 },
    "usage": { "inputTokens": 1234, "outputTokens": 567 }
  },
  "meta": { "creditsConsumed": 15 }
}

Send Message (Callback)#

For longer-running queries, use callback mode. The API returns immediately with a 202 status and delivers the AI response to your configured callback URL when processing completes.

curl -X POST https://api.gydr.ai/widgets/{widgetId}/conversations/{sessionId}/messages \
  -H "X-API-Key: api_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Compare all your laptop models by price and features",
    "response_type": "callback"
  }'

Immediate response (202 Accepted):

{
  "data": {
    "messageId": null,
    "status": "processing"
  },
  "meta": { "creditsConsumed": 0 }
}

The messageId is null in the immediate response because the message has not been processed yet. The actual messageId is included in the callback payload delivered to your callback URL.

Send Message Fields#

FieldTypeRequiredDescription
contentstringYesThe visitor's message (1–4000 chars)
response_typestringNosync (default) or callback
visitorDetailsobjectNoUnverified details (name, email, phone, company, location) to upsert onto the visitor record mid-conversation. Useful when they are captured after session creation. The person is fixed per session: a message cannot name a user.
timezonestringNoPer-turn IANA timezone override (e.g. "Asia/Kuala_Lumpur"). Falls back to the session's stored timezone when omitted. Used for time-aware responses and Dynamic Form date/time answers.

Signed-in users#

When your backend already knows who the visitor is (they are signed in to your app), create the session with their userId. Gydr derives the visitor id from it, so the same person gets the same conversation history here as on your website widget — and plugins can look up that person's own data, for example their WooCommerce orders. The id is your own id for the person, in the namespace of the widget's sign-in source; your API key is the proof.

curl -X POST https://api.gydr.ai/widgets/{widgetId}/conversations \
  -H "X-API-Key: api_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "42",
    "visitorDetails": { "name": "Siti", "email": "siti@example.com" }
  }'
  • Only an API key with the user:assert scope may send userId. A key without the scope gets 403 — the field is never silently ignored. Add the scope only to keys used by a server that authenticates your users.
  • The widget needs a sign-in source; otherwise the call returns 400 no_sign_in_source.
  • The user is fixed for the session. To continue the conversation call sendMessage with the same sessionId; resuming a session that belongs to another visitor or user returns 404. Messages and form submissions take no user fields, and an identity or visitorId in the body returns 400.
  • userId is 1–255 printable characters. Sessions and callbacks report visitorId and userId.

Callback Payload#

When using callback mode, the AI response is delivered to your callback URL as a POST request with the following payload:

{
  "event": "message.completed",
  "messageId": "msg_abc123",
  "sessionId": "sess_abc123",
  "widgetId": "6751a3e...",
  "data": {
    "content": "Here are some products...",
    "products": [
      { "id": "prod_1", "name": "Wireless Earbuds", "price": 29.99 }
    ],
    "actions": [],
    "sessionEnded": false,
    "credits": { "chat": 10, "retrieval": 2, "orchestration": 1, "embedding": 0, "rerank": 1, "plugin": 2, "pluginRerank": 0, "crawler": 0 },
    "usage": { "inputTokens": 1234, "outputTokens": 567 }
  }
}

The credits object always carries the same seven numeric keys — see Response Formats → The credits Field.

Breaking change (1.31.0)

The credits.api field was renamed to credits.plugin, and the retrieval, orchestration, and crawler buckets were added. Update any code that reads credits.api to read credits.plugin.

Callback Signature Verification#

Every callback delivery includes three headers:

  • X-Gydr-Signature — HMAC-SHA256 hex digest for verifying authenticity
  • X-Gydr-Timestamp — Unix timestamp of the delivery
  • X-Gydr-Event — event type: message.completed or message.failed

The signature is computed as HMAC-SHA256(callbackSecret, "{timestamp}.{rawBody}"). Verify it using your callback secret (provided when you create the API key with a callback URL):

import crypto from "crypto"

function verifyCallbackSignature(rawBody, headers, callbackSecret) {
  const timestamp = headers["x-gydr-timestamp"]
  const signature = headers["x-gydr-signature"]

  const expected = crypto
    .createHmac("sha256", callbackSecret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex")

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )

  // Optional: reject if timestamp is older than 5 minutes
  const age = Math.abs(Date.now() / 1000 - parseInt(timestamp))
  if (age > 300) return false

  return isValid
}

If callback delivery fails, the system retries up to 3 times with backoff intervals of 0, 1, and 5 seconds. Failed deliveries do not result in data loss — messages are stored in the database and can be retrieved via the Get Messages endpoint.

Callback URL setup

Configure a callback URL when creating your API key in Console → Settings → Developer. The callback secret is shown once at creation time. Callback mode returns a 400 error if no callback URL is configured on the API key.

Automatic session resolution

When the AI detects the conversation has concluded naturally (e.g., the visitor says "thank you"), sessionEnded is returned as true and the session is automatically resolved. You do not need to call the End Session endpoint in this case.

Submit Form#

When the AI surfaces a Dynamic Form in a response (form present), visitor answers must be submitted via this endpoint — not as a plain message. A rejected submission (wrong formId, invalid answer, or no pending form) returns 400 before any billing or agent run. Accepted submissions run the answer turn and are billed exactly like a send-message turn.

curl -X POST https://api.gydr.ai/widgets/{widgetId}/conversations/{sessionId}/form-submissions \
  -H "X-API-Key: api_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "formId": "frm_abc123",
    "answers": {
      "q1": "John Smith",
      "q2": "john@example.com"
    }
  }'

Response (same shape as send-message):

{
  "data": {
    "messageId": "msg_abc123",
    "content": "Thanks, John! We'll be in touch at john@example.com shortly.",
    "products": [],
    "actions": [],
    "sessionEnded": false,
    "credits": { "chat": 6, "retrieval": 0, "orchestration": 1, "embedding": 0, "rerank": 0, "plugin": 0, "pluginRerank": 0, "crawler": 0 },
    "usage": { "inputTokens": 890, "outputTokens": 42 }
  },
  "meta": { "creditsConsumed": 9 }
}

Reaching a human agent#

When a visitor asks for a person, sounds frustrated, or the bot keeps failing to answer, Gydr's own logic (never the model) decides what to offer. The response then carries a ready-made form as its single form and lifts its step to a top-level humanAgentStep. Show the form, then submit the visitor's answer to the form-submissions endpoint above like any other form. These answers need no model turn, so the response has the usual shape with empty products and zero model credits.

humanAgentStepWhat it asksSubmit
offerA proactive offer: "Would you like me to connect you with our team?" Sent alongside the bot's reply when the visitor sounded frustrated or the bot failed repeatedly. Nothing has been checked with any team yet.#HUMANAGENT#offer:yes or #HUMANAGENT#offer:no. No starts a three-turn cooldown with no signal.
team_pickWhich team? Returned when two or more teams are open to this visitor. The options are labelled with the team names from the Console. A signed-out visitor is never offered a team that needs sign-in. No team has been called yet.The chosen option's value (it looks like #HANDOFF#<integrationId>). Gydr checks it against the form it stored, so a team the form did not list is refused.
ticket_formA support-ticket form: email, optional name (filled in from the session's visitor details when they include one) and a summary already drafted from the conversation. The email is never filled in. Returned when the team answers by ticket rather than live chat, or no one is free to chat and the team takes tickets instead.Answers keyed email, name and summary. The ticket is filed and the response confirms it.

A team pick returned to your code looks like this (the answer to it is the chosen option's value):

{
  "data": {
    "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" }
        ]
      }]
    }
  }
}

Live chat is a signal. When the team the visitor picked (or the only team) is reachable for live chat and has an agent free, the answer carries handoff: true and the team's handoff:<integrationId> action in actions. The API never starts the live handoff itself: you own the live-chat surface and begin it. A team that opted into an availability check is asked only after the pick, with a short time limit; if it cannot confirm that an agent is free, the answer is "no one is available" (with the team's other contacts) and handoff stays false. A team with several desks never returns a bare handoff:prompt action: you get the team pick.

Tickets. Submitting the ticket form files the ticket with the team and answers with the ticket number and the address the reply will go to. A ticket costs 1 credit when it is filed (reported in credits.integration), plus the credits for the summary draft (reported in credits.ticketSummary; none when a draft was reused or could not be made — a charged draft is always the summary the form arrives with). Every answer to a human-agent form also carries the usual per-request surcharge, and meta.creditsConsumed is the total. A ticket started before the balance ran out can still be filed. Ticket requests carry no files: attachments are never forwarded to a ticket.

Errors. A form that is not one of the three steps above (the API never receives the website's chat-or-ticket mode choice) is refused with 400 UNSUPPORTED_ORIGIN on formId and stays pending. A malformed answer is refused with a 400 that names the field.

End Session#

Resolves an active session. After ending, no more messages can be sent. While a human agent is handling the conversation, this call is refused with 409 (and no credit is used) only when the business turned off "Visitors can leave a live chat" for that team in the Console. It is on by default, and then the call ends the session, the response's resolution is visitor_left_handoff, and the team's ticket is closed (the default) or kept open if the business chose that. Show your own "you left the conversation" line; the API returns none.

curl -X POST https://api.gydr.ai/widgets/{widgetId}/conversations/{sessionId}/end \
  -H "X-API-Key: api_your_api_key"

Response:

{
  "data": {
    "sessionId": "sess_abc123",
    "status": "resolved",
    "resolution": "manual",
    "resolvedAt": "2026-03-23T10:15:00.000Z"
  },
  "meta": { "creditsConsumed": 1 }
}

Rate Session#

Submits a satisfaction rating for a resolved or expired session. Only positive and negative values are accepted.

curl -X POST https://api.gydr.ai/widgets/{widgetId}/conversations/{sessionId}/rate \
  -H "X-API-Key: api_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "rating": "positive" }'

Response:

{
  "data": {
    "sessionId": "sess_abc123",
    "rating": "positive"
  },
  "meta": { "creditsConsumed": 1 }
}

Get Session#

Retrieves the current state and metadata of a session.

curl https://api.gydr.ai/widgets/{widgetId}/conversations/{sessionId} \
  -H "X-API-Key: api_your_api_key"

Response:

{
  "data": {
    "sessionId": "sess_abc123",
    "widgetId": "6751a3e...",
    "visitorId": "usr_3f9c1a7e5b2d48a6b0c4e8f21d7a9b35",
    "userId": "42",
    "status": "active",
    "messageCount": 5,
    "rating": null,
    "createdAt": "2026-03-23T10:00:00.000Z",
    "resolvedAt": null
  },
  "meta": { "creditsConsumed": 1 }
}

Get Messages#

Retrieves the message history for a session. Supports cursor-based pagination for sessions with many messages.

curl "https://api.gydr.ai/widgets/{widgetId}/conversations/{sessionId}/messages?limit=20" \
  -H "X-API-Key: api_your_api_key"

# Next page (use cursor from previous response)
curl "https://api.gydr.ai/widgets/{widgetId}/conversations/{sessionId}/messages?limit=20&cursor=eyJ0..." \
  -H "X-API-Key: api_your_api_key"

Response:

{
  "data": [
    {
      "messageId": "msg_001",
      "role": "user",
      "content": "What products do you have under $50?",
      "createdAt": "2026-03-23T10:01:00.000Z"
    },
    {
      "messageId": "msg_002",
      "role": "assistant",
      "content": "Here are some products under $50...",
      "products": [
        { "id": "prod_1", "name": "Wireless Earbuds", "price": 29.99 }
      ],
      "createdAt": "2026-03-23T10:01:05.000Z"
    }
  ],
  "pagination": { "cursor": "eyJ0...", "hasMore": true },
  "meta": { "creditsConsumed": 1 }
}

Every response carries meta.creditsConsumed so you can attribute metered usage per request. What each operation costs is documented in How Credits Work.

Session & Visitor IDs#

You can choose the session id; Gydr always makes the visitor id:

  • visitorId — made by Gydr and returned by createSession, sendMessage, getSession and the callbacks. A guest id starts vis_, a signed-in person's starts usr_. You cannot choose one; to correlate with your own user database send userId.
  • sessionId — use your own identifier for tracking (e.g., support ticket ID, thread ID). Must be unique per widget. If omitted, the server generates a sess_ prefixed ID.

Error Scenarios#

StatusCauseResolution
400Callback URL not configured for this API key (response_type: "callback" with no callback URL set)Set a callback URL in Console → Settings → Developer when creating your API key
400protocol_value_not_a_message on content: the message is an action or handoff form value (for example handoff:int_support) posted back to GydrStart a live handoff on your side, answer forms through form-submissions, end through the end endpoint. No credit is used
402Account balance exhausted or payment overdueResolve the account balance in Console
404Session or widget not foundVerify the widget ID and session ID are correct
409Session ID already used for a resolved or expired sessionUse a new session ID or omit it to auto-generate
409Messages, form answers or End on a session a human agent is handling (End is allowed only where the team lets visitors leave)Wait for the agent to close the conversation; no credit is used

See the Error Handling page for the full error response format and general error codes.