External Conversation Sync
If your product already has its own database of record for conversations — a support ticket system, a student records system, your own analytics pipeline — you don’t have to treat Wetel’s own session/message store as the source of truth. External Conversation Sync lets Wetel best-effort mirror every chat turn to your own GraphQL API as it happens.
This is a one-way, fire-and-forget mirror: Wetel calls out to your backend after each turn; your backend does not call back into Wetel.
What it assumes about your backend
Section titled “What it assumes about your backend”Wetel expects your GraphQL API to expose two mutations:
createOneConversation(input: ...)— called once per new conversation, returning at least{ id }.createOneConversationMessage(input: ...)— called once per turn (the user’s message, then the assistant’s reply), returning at least{ id }.
If your backend doesn’t already have mutations shaped like this, you’ll need to add them — or configure Wetel to call whatever your equivalent mutations and types are named (see Configuring the schema shape below).
Turning it on
Section titled “Turning it on”Set three fields on your agent via updateAgent:
mutation UpdateAgent($id: ID!, $input: UpdateAgentInput!) { updateAgent(id: $id, input: $input) { id externalSyncUrl }}{ "id": "<your-agent-id>", "input": { "externalSyncUrl": "https://your-backend.example.com/graphql", "externalSyncOwnerId": 123 }}Remember the x-huat-platform: customer header on this request, as with every Wetel mutation.
externalSyncUrl— the GraphQL endpoint on your own backend that Wetel will call. Must be a valid URL (validated at the GraphQL layer as of 2026-09-08 — a malformed value is now rejected onupdateAgentbefore it ever reaches the database). It’s also checked against the same private/internal-address guard every other tenant-controlled outbound URL uses, both when you save it and again on every actual call — a hostname that doesn’t resolve, or resolves to a private/loopback/reserved range, is rejected. Redirects from your endpoint are re-validated the same way, not followed blindly.externalSyncOwnerId— an integer identifier meaningful to your schema (e.g. a student ID, a customer ID) — Wetel passes it through untouched as part of the conversation payload; it does not interpret this value itself.externalSyncSecretRef(optional) — an opaque reference string used to authenticate the outbound call to your backend, if it requires one. This is a reference, not a raw credential — the actual secret value is stored separately and never accepted or returned as plaintext through this field. As of 2026-09-08, a secret ref must belong to your own tenant’s external-sync namespace — this was already the intent, and is now enforced server-side rather than only by convention.
Configuring the schema shape (externalSyncSchemaConfig)
Section titled “Configuring the schema shape (externalSyncSchemaConfig)”Every integrator’s backend names its mutation input types and role-enum values a little differently. Rather than hardcoding one convention, Wetel lets you override the four values that vary:
type ExternalSyncSchemaConfig { conversationInputTypeName: String messageInputTypeName: String userRoleValue: String assistantRoleValue: String}| Field | Default | What it controls |
|---|---|---|
conversationInputTypeName | CreateConversationInput | The GraphQL input type name for createOneConversation’s input argument. |
messageInputTypeName | CreateConversationMessageInput | The GraphQL input type name for createOneConversationMessage’s input argument. |
userRoleValue | USER | The literal value sent for the message’s role field when the turn is the end user’s message. |
assistantRoleValue | ASSISTANT | The literal value sent for the message’s role field when the turn is the agent’s reply. |
Set these the same way as the other sync fields, via updateAgent:
mutation UpdateAgent($id: ID!, $input: UpdateAgentInput!) { updateAgent(id: $id, input: $input) { id externalSyncSchemaConfig { conversationInputTypeName messageInputTypeName userRoleValue assistantRoleValue } }}{ "id": "<your-agent-id>", "input": { "externalSyncSchemaConfig": { "conversationInputTypeName": "CreateOneConversationInput", "messageInputTypeName": "CreateOneConversationMessageInput", "userRoleValue": "user", "assistantRoleValue": "assistant" } }}Any field you omit falls back to the default shown above — you only need to override the ones that actually differ from your backend’s schema.
conversationInputTypeName/messageInputTypeName must be a valid GraphQL type name (letters, digits, underscores, not starting with a digit) — as of 2026-09-08 this is enforced server-side, since these two values are spliced directly into the outbound GraphQL document text Wetel sends to your backend. userRoleValue/assistantRoleValue have no such restriction — they’re sent as ordinary GraphQL variables, never interpolated into the document itself.
Delivery guarantees
Section titled “Delivery guarantees”External sync is best-effort, by design:
- Each sync is queued to a background job (not executed inline) with a small number of retries and a short timeout.
- Sync never blocks or delays the actual chat reply — your end user’s conversation continues at full speed regardless of whether your backend is reachable.
- If your backend is briefly down or times out, that turn’s sync attempt is simply dropped after retries are exhausted — it is not retried indefinitely, and there’s no replay mechanism for turns missed during an outage.
Because of this, treat external sync as a convenience mirror for analytics, reporting, or your own record-keeping — not as a transactionally-guaranteed data pipeline. If you need guaranteed delivery, poll or query Wetel’s own conversation data directly instead of relying solely on the sync push.
If you need stronger guarantees than a best-effort webhook mirror can offer, consider Backend-Proxied Relay Architecture instead — your own backend intercepts every message directly as it happens, rather than receiving a fire-and-forget copy after the fact. That’s a bigger architectural commitment (your backend becomes a mandatory hop for every message), so it’s worth reading both pages before choosing.
Common Integration Pitfalls
Section titled “Common Integration Pitfalls”Your mutation’s input type name might not match Wetel’s default assumption. Wetel’s defaults (CreateConversationInput/CreateConversationMessageInput) match a hand-rolled GraphQL resolver’s typical naming convention — but if your backend uses a code-generation tool that names input types differently (for example, prefixing with CreateOne), the default names won’t resolve and calls will fail with a GraphQL “unknown type” error. Before configuring sync, introspect your own schema to confirm the exact input type names:
query { __type(name: "CreateConversationInput") { name inputFields { name type { name } } }}If that returns null, your type is named something else — find the real name and set it via conversationInputTypeName/messageInputTypeName.
Role-enum casing might differ. A common mismatch: Wetel’s defaults send USER/ASSISTANT (uppercase), matching the common GraphQL convention of serializing enums by their key. If your backend’s role enum uses lowercase values (user/assistant) or entirely custom values (e.g. HUMAN/AGENT), calls will fail with a “value does not exist in enum” error. Set userRoleValue/assistantRoleValue to match your backend’s actual enum values exactly — introspect your enum type if you’re not sure:
query { __type(name: "YourRoleEnumName") { enumValues { name } }}Your backend may need to accept a custom platform header. If your backend was built from infrastructure conventions similar to Wetel’s own (for example, a shared internal starter template with a global request-guarding interceptor), it may reject any request missing a specific header — analogous to how Wetel itself requires x-huat-platform: customer on every request. Wetel’s sync calls always send x-huat-platform: customer on outbound sync requests, which is harmless for backends that don’t check it but resolves a rejected-request issue for backends that do. If your sync calls are failing with a generic “forbidden” or “platform not specified” style error and the mutation/role names are otherwise correct, check whether your own backend has a similar global guard and what header name/value it expects.
See also
Section titled “See also”- Embeddings API — use Wetel as a standalone embeddings provider.
- Agents — how agents and their configuration fields fit together.
- MCP Connectors — registering external tools for an agent’s workflow to call.
- Multi-Tenancy — how tenant isolation applies to agent configuration.
- Troubleshooting — general API error reference.