On this page
- Conversations API
- Endpoints
- Session Lifecycle
- Response Modes
- Create Session
- Create Session Fields
- Send Message (Sync)
- Send Message (Callback)
- Send Message Fields
- Signed-in users
- Callback Payload
- Callback Signature Verification
- Submit Form
- Reaching a human agent
- End Session
- Rate Session
- Get Session
- Get Messages
- Session & Visitor IDs
- Error Scenarios
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#
| Method | Path |
|---|---|
| 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:
- Create Session — initialize a new chat session for a visitor
- Send Messages — exchange messages back and forth with the AI
- End Session — resolve the session when the conversation is complete
- 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.
Response Modes#
The Send Message endpoint supports two response types:
| Type | Behavior | Best For |
|---|---|---|
sync | Synchronous. Waits for the AI response before returning (~25s timeout). | Simple queries, real-time chat UIs |
callback | Asynchronous. 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#
| Field | Type | Required | Description |
|---|---|---|---|
visitorId | string | No | Omit 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. |
userId | string | No | Your 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. |
sessionId | string | No | Custom session ID. Server generates one if omitted. |
visitorDetails | object | No | Unverified details: name, email, phone, company, location (latitude and longitude, optional name) |
language | string | No | BCP 47 language code (e.g., "en", "ms", "zh"). AI responds in this language initially. |
timezone | string | No | IANA 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#
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | The visitor's message (1–4000 chars) |
response_type | string | No | sync (default) or callback |
visitorDetails | object | No | Unverified 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. |
timezone | string | No | Per-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:assertscope may senduserId. 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
sendMessagewith the samesessionId; resuming a session that belongs to another visitor or user returns 404. Messages and form submissions take no user fields, and anidentityorvisitorIdin the body returns 400. userIdis 1–255 printable characters. Sessions and callbacks reportvisitorIdanduserId.
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 authenticityX-Gydr-Timestamp— Unix timestamp of the deliveryX-Gydr-Event— event type:message.completedormessage.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.
| humanAgentStep | What it asks | Submit |
|---|---|---|
offer | A 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_pick | Which 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_form | A 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,getSessionand the callbacks. A guest id startsvis_, a signed-in person's startsusr_. You cannot choose one; to correlate with your own user database senduserId. - 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#
| Status | Cause | Resolution |
|---|---|---|
| 400 | Callback 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 |
| 400 | protocol_value_not_a_message on content: the message is an action or handoff form value (for example handoff:int_support) posted back to Gydr | Start a live handoff on your side, answer forms through form-submissions, end through the end endpoint. No credit is used |
| 402 | Account balance exhausted or payment overdue | Resolve the account balance in Console |
| 404 | Session or widget not found | Verify the widget ID and session ID are correct |
| 409 | Session ID already used for a resolved or expired session | Use a new session ID or omit it to auto-generate |
| 409 | Messages, 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.
