MonCha
Skip to article

Enterprise Vendor API

Search MonCha documentation

Search the full guides for topics such as renewal, ticket alerts, OAuth or knowledge refresh.

Offer customer-owned MonCha bots from your enterprise application: connect, create, train and manage.

On this page

Offer MonCha bots from your own application. Any enterprise with API Vendor enabled can connect customers, create customer-owned bots, configure them, and train their knowledge bases through a separate vendor API.

Enable and set up your application

API Vendor is an optional enterprise feature, off by default. Ask your MonCha administrator to enable it on your contract. After successful payment, an enterprise owner or administrator can open Dashboard → API Vendor to configure the application name, logo, allowed origins and exact callback URLs, generate a key, and view connected customers and sponsored bots.

The reference is public. Credentials and enterprise setup remain in the authenticated paid portal. The portal also provides vendor-api.txt; it is not attached to the invitation email.

Connect → create → train → install

  1. Your customer clicks Connect MonCha in your application. Your backend creates a connection session with their local customer ID, a random state value and a PKCE S256 challenge.
  2. Open the returned authorization URL in a browser popup or redirect. The MonCha × your application screen lets the customer sign in or sign up and approve access.
  3. Your backend verifies callback state against the same signed-in customer session, then exchanges the single-use code with its PKCE verifier. Store the returned connection ID against that customer.
  4. Send the bot name and Starter, Growth or Scale tier. MonCha checks your enterprise capacity and creates the bot in the customer’s personal MonCha account.
  5. Configure bot settings, widget and allowed domains. Submit URLs, text or files, then poll the returned training ID to distinguish ready, processing and failed documents.
  6. Use the returned customer tenant ID and bot ID to install the widget. The enterprise tenant ID is used for backend authentication.

Backend authentication

Keep the key and enterprise tenant ID in server secrets. Every vendor request requires both headers below. Browser code calls your backend; it must never receive the vendor key. Access also requires the customer’s consent and the requested scope.

POST https://api.dev-moncha.com/vendor/v1/customers/CONNECTION_ID/bots
Authorization: Bearer YOUR_VENDOR_SERVER_SECRET
X-Vendor-Tenant-Id: YOUR_ENTERPRISE_TENANT_ID
Content-Type: application/json
Idempotency-Key: customer-support-bot-001

{"name":"Support","plan_tier":"growth","language":"en"}

Use a stable idempotency key when retrying bot creation or ingestion. Do not reuse it with a different payload. Rotate credentials in the enterprise portal. These credentials are separate from workspace API and MCP tokens.

Ownership, slots and payment

  • Customers can edit their sponsored bots directly in MonCha. Your enterprise can manage only bots sponsored through its customer connections.
  • Enterprise-owned and sponsored bots share the contract’s Starter, Growth and Scale slots. Each bot has its own tier quota. Deleting a sponsored bot returns its slot once.
  • Enterprise deactivation stops sponsored service while retaining the bot’s data and slot. A lapsed monthly or annual contract also stops funded service.
  • Disconnection revokes vendor access and ends sponsorship. Disconnection, deletion and completed personal payment takeover release the sponsored slot.
  • Customers can continue with normal MonCha billing through Sponsored bots in their dashboard. A completed takeover keeps their bot ID and data and removes vendor access. Payment confirmation, rather than the checkout redirect alone, activates service.

Existing direct MonCha signup and enterprise workspace flows continue as before. A new customer account created through vendor authorization starts without the direct signup trial. MonCha’s internal administration records the referring enterprise.

Supported operations and boundaries

The reference covers bot settings, widget appearance, domains, installation, usage, knowledge discovery and ingestion, document updates and retraining, training status, analytics, leads, conversations, tickets, bookings, contacts, summaries, existing inbox threads, functions, webhooks, provider imports and bring-your-own WhatsApp connections. Tier and contract entitlements still apply.

Notion and Google imports need separate provider consent. Personal billing, account passwords, team administration, contract changes and new phone-number, inbox, branding or add-on purchases are outside the vendor API. Training uses polling. Existing event webhooks use best-effort delivery without durable retries.

API reference and downloads

Contract version: 2026-09-26. All endpoint paths below use /vendor/v1. Download the complete text guide or OpenAPI 3.1 specification for schemas, success codes, error types and integration examples.

GET /account

Enterprise name and vendor tenant ID

Scope: customers:read

Request

none

Response

{ tenant_id: UUID, name: string }
GET /slots

Contract capacity and occupied slots by tier

Scope: bots:read

Request

none

Response

{ data: { starter: Slot, growth: Slot, scale: Slot } }
POST /connect/sessions

Start branded browser authorization

Scope: connect:write

Request

{ external_customer_id: string(1..200), redirect_uri: registered URL, state: string(32..256), code_challenge: PKCE S256 base64url(43), scopes?: Scope[] }

Response

{ authorization_url: URL, expires_in: 600 }
POST /connect/exchange

Exchange a single-use code on your backend

Scope: connect:write

Request

{ code: hex(64), code_verifier: PKCE string(43..128), redirect_uri: registered URL }

Response

{ connection_id: UUID, customer_tenant_id: UUID, customer?: { name: string, email: string } }; profile requires customers:read consent
GET /customers

List your active connected customers

Scope: customers:read

Request

query: cursor?: UUID, limit?: integer(1..100), default 50

Response

{ data: Customer[], next_cursor: UUID | null }
GET /customers/{id}

Read your connected customer

Scope: customers:read

Request

none

Response

{ data: Customer }
DELETE /customers/{id}/connection

Disconnect; revoke access and stop sponsorship

Scope: connect:write

Request

none

Response

{ revoked: true }
GET /customers/{id}/bots

List that customer’s bots sponsored by you (up to 100)

Scope: bots:read

Request

none

Response

{ data: Bot[] }
POST /customers/{id}/bots

Create a customer-owned bot and reserve one tier slot

Scope: bots:write

Request

{ name: string, plan_tier: starter | growth | scale, language?: string = en }; Idempotency-Key required

Response

{ data: { bot_id: UUID, customer_tenant_id: UUID, connection_id: UUID, plan_tier: Tier, funding: vendor, install: { tenantId: UUID, chatbotId: UUID } } }
GET /bots/{botId}

Read a sponsored bot

Scope: bots:read

Request

none

Response

{ data: Bot }
PATCH /bots/{botId}

Edit bot name/publication/autosync

Scope: bots:write

Request

{ name?: string, is_active?: boolean, auto_sync_enabled?: boolean }

Response

{ data: Bot }
DELETE /bots/{botId}

Delete bot; return its tier slot exactly once

Scope: bots:write

Request

none

Response

{ deleted: UUID }
POST /bots/{botId}/deactivate

Stop sponsored live service; retain slot and data

Scope: bots:write

Request

none

Response

{ ok: true }
POST /bots/{botId}/reactivate

Resume with active payment, consent and slot

Scope: bots:write

Request

none

Response

{ ok: true }
PATCH /bots/{botId}/plan

Change tier atomically; retain used quota

Scope: bots:write

Request

{ plan_tier: Tier }

Response

{ ok: true }
GET /bots/{botId}/settings

Read bot behavior settings

Scope: bots:read

Request

none

Response

{ settings: BotSettings }
PUT /bots/{botId}/settings

Save bot behavior settings

Scope: bots:write

Request

BotSettings (same schema as MonCha bot settings)

Response

{ settings: BotSettings }
GET /bots/{botId}/widget

Read widget appearance

Scope: bots:read

Request

none

Response

{ config: WidgetConfig }
PUT /bots/{botId}/widget

Save widget appearance

Scope: bots:write

Request

WidgetConfig; hideBranding=true is excluded from vendor API purchases and returns branding_locked

Response

{ config: WidgetConfig }
GET /bots/{botId}/domains

Read allowed embed hostnames

Scope: bots:read

Request

none

Response

{ domains: string[] }
PUT /bots/{botId}/domains

Set allowed embed domains; empty means unrestricted

Scope: bots:write

Request

{ domains: string[0..20] }

Response

{ domains: string[] }
GET /bots/{botId}/install

Get the CUSTOMER tenant and bot IDs for installation

Scope: bots:read

Request

none

Response

{ tenantId: UUID, chatbotId: UUID, apiUrl: URL, widgetUrl: URL | null }
GET /bots/{botId}/usage

Read isolated tier usage and funding

Scope: bots:read

Request

none

Response

{ funding: vendor, live: boolean, plan_tier: Tier, limits: Plan, usage: { messages: number, voice_minutes: number, pages: number } }
POST /bots/{botId}/knowledge/ingest

Queue one URL/text document

Scope: kb:write

Request

{ documents: [{ title: string(1..300), url?: public URL, text?: string(1..200000), source_type?: url | qa | snippet }] }; exactly one url/text; Idempotency-Key required

Response

{ data: { training_id: UUID, status: queued, total: number } }
POST /bots/{botId}/knowledge/ingest-bulk

Queue up to 100 selected documents

Scope: kb:write

Request

{ documents: DocumentInput[1..100] }; Idempotency-Key required

Response

{ data: { training_id: UUID, status: queued, total: number } }
GET /bots/{botId}/knowledge

List knowledge documents and status

Scope: kb:read

Request

none

Response

{ data: KnowledgeDocument[] }
GET /bots/{botId}/training/{trainingId}

Poll tracked training progress

Scope: kb:read

Request

none

Response

{ training_id: UUID, total: number, ready: number, failed: number, status: processing | completed | partial_failure | failed, documents: KnowledgeDocument[], poll_after_seconds: 5 }
POST /bots/{botId}/knowledge/discover

Find website pages without starting training

Scope: kb:write

Request

{ url: public URL, max_pages?: integer(1..100)=100, include_paths?: string[0..20], exclude_paths?: string[0..20] }

Response

{ urls: URL[], pagesFetched: number, truncated: boolean, next_step: string }
POST /bots/{botId}/knowledge/upload

Upload PDF, DOCX, XLSX, XLS, TXT, CSV or Markdown (10 MiB maximum)

Scope: kb:write

Request

Raw file bytes; X-File-Name: percent-encoded filename; Idempotency-Key required

Response

{ data: { training_id: UUID, status: queued, total: number } }
GET /bots/{botId}/knowledge/{documentId}

Read one document, its content and current status

Scope: kb:read

Request

none

Response

{ data: KnowledgeDocument }
DELETE /bots/{botId}/knowledge/{documentId}

Delete one document and return its page usage

Scope: kb:write

Request

none

Response

{ deleted: UUID }
PATCH /bots/{botId}/knowledge/{documentId}

Edit and retrain a QA/snippet source; queued documents keep their in-progress job

Scope: kb:write

Request

{ title?: string(1..300), text: string(1..200000) }

Response

{ data: { training_id: UUID, status: queued | processing, document_id: UUID } }
POST /bots/{botId}/knowledge/{documentId}/retrain

Retrain using the stored source; active jobs are reused

Scope: kb:write

Request

none

Response

{ data: { training_id: UUID, status: queued | processing, document_id: UUID } }
GET /bots/{botId}/analytics

Read this bot’s analytics. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.

Scope: operations:read

Request

none

Response

{ data: analytics[] | object, settings?: object }
GET /bots/{botId}/leads

Read this bot’s leads. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.

Scope: operations:read

Request

none

Response

{ data: leads[] | object, settings?: object }
GET /bots/{botId}/conversations

Read this bot’s conversations. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.

Scope: operations:read

Request

none

Response

{ data: conversations[] | object, settings?: object }
GET /bots/{botId}/tickets

Read this bot’s tickets. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.

Scope: operations:read

Request

none

Response

{ data: tickets[] | object, settings?: object }
GET /bots/{botId}/contacts

Read this bot’s contacts. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.

Scope: operations:read

Request

none

Response

{ data: contacts[] | object, settings?: object }
GET /bots/{botId}/bookings

Read this bot’s bookings. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.

Scope: operations:read

Request

none

Response

{ data: bookings[] | object, settings?: object }
GET /bots/{botId}/summaries

Read this bot’s summaries. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.

Scope: operations:read

Request

none

Response

{ data: summaries[] | object, settings?: object }
GET /bots/{botId}/conversations/{sessionId}

Read up to 1,000 messages belonging to this bot and session

Scope: operations:read

Request

none

Response

{ data: { id: UUID, role: string, content: string, created_at: ISODate, feedback: string | null }[] }
GET /bots/{botId}/inbox

Read up to 80 existing email inbox threads; does not purchase or enable an inbox

Scope: operations:read

Request

none

Response

{ data: InboxThread[] }
GET /bots/{botId}/inbox/{id}

Read up to 1,000 messages in this bot’s email thread

Scope: operations:read

Request

none

Response

{ data: Message[] }
PATCH /bots/{botId}/leads/{id}

Edit lead status and notes

Scope: operations:write

Request

{ status?: new | contacted | qualified | won | lost, notes?: string(0..10000) }

Response

{ data: Lead }
GET /bots/{botId}/tickets/{id}/messages

Read a ticket thread belonging to this bot

Scope: operations:read

Request

none

Response

{ data: TicketMessage[] }
PATCH /bots/{botId}/tickets/{id}

Update ticket status

Scope: operations:write

Request

{ status: open | in_progress | resolved | closed }

Response

{ data: Ticket }
POST /bots/{botId}/tickets/{id}/messages

Reply as the enterprise support team; visible to the visitor

Scope: operations:write

Request

{ body: string(1..10000) }

Response

{ data: TicketMessage }
PATCH /bots/{botId}/bookings/{id}

Confirm/reject/reschedule/cancel using MonCha booking rules; vendor handles its own customer notifications

Scope: operations:write

Request

{ action: confirm | reject | reschedule | cancel, requestedSlot?: ISODate, rejectionComment?: string(0..1000), cancelComment?: string(0..1000) }

Response

{ data: Booking }
PATCH /bots/{botId}/summaries/settings

Set the bot’s summary cadence

Scope: operations:write

Request

{ interval_days: 30 | 90 }

Response

{ ok: true }
PUT /bots/{botId}/notifications

Set bot notification destinations; vendor overrides clear on disconnect/takeover

Scope: bots:write

Request

{ notify_email: email | null, ticket_alert_emails: email[0..10] }

Response

{ ok: true }
GET /bots/{botId}/functions

List only functions created through this vendor connection; secrets are omitted

Scope: integrations:manage

Request

none

Response

{ data: Function[] }
DELETE /bots/{botId}/functions/{id}

Delete a vendor-owned functions resource

Scope: integrations:manage

Request

none

Response

{ deleted: UUID }
GET /bots/{botId}/webhooks

List only webhooks created through this vendor connection; secrets are omitted

Scope: integrations:manage

Request

none

Response

{ data: Webhook[] }
DELETE /bots/{botId}/webhooks/{id}

Delete a vendor-owned webhooks resource

Scope: integrations:manage

Request

none

Response

{ deleted: UUID }
POST /bots/{botId}/functions

Create a bot-only function, subject to tier and enterprise API entitlement

Scope: integrations:manage

Request

{ name: lowercase identifier(2..61), description: string(10..500), method: GET | POST, url: HTTPS URL, headers?: Record<string,string>, params: Record<string,string> }

Response

{ data: Function }
POST /bots/{botId}/webhooks

Create a bot-only webhook; the signing secret is shown once; requires tier and contract webhooks

Scope: integrations:manage

Request

{ url: HTTPS URL, events: (lead.created | conversation.handoff | message.unanswered | ticket.created | booking.created)[] }

Response

{ data: Webhook & { secret: string } }
POST /bots/{botId}/integrations/{provider}/authorize

Start separate provider consent for notion or google; never uses customer-wide connections

Scope: integrations:manage

Request

{ redirect_uri: registered URL, state: string(32..256) }

Response

{ authorization_url: URL, expires_in: 600 }
GET /bots/{botId}/integrations/{provider}

List pages/files available through this bot/vendor’s provider grant

Scope: integrations:manage

Request

none

Response

{ data: { id: string, title: string, url?: URL, mimeType?: string }[] }
DELETE /bots/{botId}/integrations/{provider}

Delete this scoped provider grant; imported documents are retained

Scope: integrations:manage

Request

none

Response

{ disconnected: true }
POST /bots/{botId}/integrations/{provider}/import

Import up to 20 selected pages/files as editable text snapshots

Scope: integrations:manage + kb:write

Request

{ items: { id: string, title: string(1..300), mime_type?: string }[1..20] }; Idempotency-Key required

Response

{ data: { training_id: UUID, status: queued, total: number } }
GET /bots/{botId}/channels/whatsapp

Read this vendor’s bring-your-own WhatsApp connection

Scope: integrations:manage

Request

none

Response

{ data: Channel | null }
POST /bots/{botId}/channels/whatsapp/connect

Start provider-hosted connection in a dedicated bot profile; does not buy numbers

Scope: integrations:manage

Request

{ redirect_uri: registered URL }

Response

{ authorization_url: URL }
POST /bots/{botId}/channels/whatsapp/complete

Complete after provider consent; retry 409 connect_incomplete after five seconds

Scope: integrations:manage

Request

none

Response

{ data: { id: UUID, status: active, phone_number: string | null } }
PATCH /bots/{botId}/channels/whatsapp

Configure the vendor-owned channel

Scope: integrations:manage

Request

{ aiEnabled?: boolean, showOnWidget?: boolean }

Response

{ ok: true }
DELETE /bots/{botId}/channels/whatsapp

Disconnect the vendor-owned channel

Scope: integrations:manage

Request

none

Response

{ disconnected: true }
Complete flow, types, errors and backend/browser examples
MonCha API Vendor — 2026-09-26
Base URL: https://api.moncha.co/vendor/v1

Available after successful enterprise payment with API - vendor enabled.
Store MONCHA_VENDOR_API_KEY and MONCHA_VENDOR_TENANT_ID only on your backend.
Every request requires Authorization: Bearer mcv_live_<secret> and X-Vendor-Tenant-Id: <enterprise tenant UUID>.
JSON writes require Content-Type: application/json. Keys never go in URLs, browser JavaScript, widgets, or emails.

FLOW
1. Save your application name/logo, exact callback URLs and origins in Dashboard → API Vendor.
2. Generate a key (shown once).
3. Your signed-in customer's backend creates random state and a PKCE S256 challenge/verifier.
4. POST /connect/sessions. Open authorization_url in a browser popup or full-page redirect. MonCha displays MonCha × your application.
5. The customer signs in/signs up and consents. On your registered callback, verify state and exchange code from the backend with the verifier. Never send MonCha a customer password.
6. Store connection_id against your authenticated local customer. Create a bot with name and tier.
7. Configure settings/domains, queue knowledge, and poll training status every five seconds.
8. Install with returned customer tenantId and chatbotId. Enterprise tenant ID is ONLY for backend vendor authentication.
9. Customers own/edit their bots in MonCha. You manage only your sponsored bots.
10. Deactivation stops funded service and keeps the slot. Deletion returns one tier slot. Disconnection stops service and revokes access.
11. When enterprise coverage ends, customers see payment required. They may adopt the SAME bot through normal MonCha billing; after takeover you cannot access it.

TYPE DEFINITIONS
UUID = UUID string. Tier = starter | growth | scale. Slot = { allocated: number; used: number; available: number }.
All timestamps are ISO 8601 strings. A missing optional field is omitted, not null, unless documented nullable.
Customer = { connection_id: UUID; customer_tenant_id: UUID; external_customer_id: string; connected_at: string; name: string; email: string }.
Bot = { bot_id: UUID; customer_tenant_id: UUID; name: string; plan_tier: Tier; is_active: boolean; vendor_deactivated_at: string | null }.
KnowledgeDocument = { id: UUID; title: string; status: queued | processing | ready | failed; chunk_count: number; pages_count: number }.

GET /account
Enterprise name and vendor tenant ID
Scope: customers:read
Request: none
Response: { tenant_id: UUID, name: string }
Success: 200

GET /slots
Contract capacity and occupied slots by tier
Scope: bots:read
Request: none
Response: { data: { starter: Slot, growth: Slot, scale: Slot } }
Success: 200

POST /connect/sessions
Start branded browser authorization
Scope: connect:write
Request: { external_customer_id: string(1..200), redirect_uri: registered URL, state: string(32..256), code_challenge: PKCE S256 base64url(43), scopes?: Scope[] }
Response: { authorization_url: URL, expires_in: 600 }
Success: 201

POST /connect/exchange
Exchange a single-use code on your backend
Scope: connect:write
Request: { code: hex(64), code_verifier: PKCE string(43..128), redirect_uri: registered URL }
Response: { connection_id: UUID, customer_tenant_id: UUID, customer?: { name: string, email: string } }; profile requires customers:read consent
Success: 200

GET /customers
List your active connected customers
Scope: customers:read
Request: query: cursor?: UUID, limit?: integer(1..100), default 50
Response: { data: Customer[], next_cursor: UUID | null }
Success: 200

GET /customers/{id}
Read your connected customer
Scope: customers:read
Request: none
Response: { data: Customer }
Success: 200

DELETE /customers/{id}/connection
Disconnect; revoke access and stop sponsorship
Scope: connect:write
Request: none
Response: { revoked: true }
Success: 200

GET /customers/{id}/bots
List that customer’s bots sponsored by you (up to 100)
Scope: bots:read
Request: none
Response: { data: Bot[] }
Success: 200

POST /customers/{id}/bots
Create a customer-owned bot and reserve one tier slot
Scope: bots:write
Request: { name: string, plan_tier: starter | growth | scale, language?: string = en }; Idempotency-Key required
Response: { data: { bot_id: UUID, customer_tenant_id: UUID, connection_id: UUID, plan_tier: Tier, funding: vendor, install: { tenantId: UUID, chatbotId: UUID } } }
Success: 201

GET /bots/{botId}
Read a sponsored bot
Scope: bots:read
Request: none
Response: { data: Bot }
Success: 200

PATCH /bots/{botId}
Edit bot name/publication/autosync
Scope: bots:write
Request: { name?: string, is_active?: boolean, auto_sync_enabled?: boolean }
Response: { data: Bot }
Success: 200

DELETE /bots/{botId}
Delete bot; return its tier slot exactly once
Scope: bots:write
Request: none
Response: { deleted: UUID }
Success: 200

POST /bots/{botId}/deactivate
Stop sponsored live service; retain slot and data
Scope: bots:write
Request: none
Response: { ok: true }
Success: 200

POST /bots/{botId}/reactivate
Resume with active payment, consent and slot
Scope: bots:write
Request: none
Response: { ok: true }
Success: 200

PATCH /bots/{botId}/plan
Change tier atomically; retain used quota
Scope: bots:write
Request: { plan_tier: Tier }
Response: { ok: true }
Success: 200

GET /bots/{botId}/settings
Read bot behavior settings
Scope: bots:read
Request: none
Response: { settings: BotSettings }
Success: 200

PUT /bots/{botId}/settings
Save bot behavior settings
Scope: bots:write
Request: BotSettings (same schema as MonCha bot settings)
Response: { settings: BotSettings }
Success: 200

GET /bots/{botId}/widget
Read widget appearance
Scope: bots:read
Request: none
Response: { config: WidgetConfig }
Success: 200

PUT /bots/{botId}/widget
Save widget appearance
Scope: bots:write
Request: WidgetConfig; hideBranding=true is excluded from vendor API purchases and returns branding_locked
Response: { config: WidgetConfig }
Success: 200

GET /bots/{botId}/domains
Read allowed embed hostnames
Scope: bots:read
Request: none
Response: { domains: string[] }
Success: 200

PUT /bots/{botId}/domains
Set allowed embed domains; empty means unrestricted
Scope: bots:write
Request: { domains: string[0..20] }
Response: { domains: string[] }
Success: 200

GET /bots/{botId}/install
Get the CUSTOMER tenant and bot IDs for installation
Scope: bots:read
Request: none
Response: { tenantId: UUID, chatbotId: UUID, apiUrl: URL, widgetUrl: URL | null }
Success: 200

GET /bots/{botId}/usage
Read isolated tier usage and funding
Scope: bots:read
Request: none
Response: { funding: vendor, live: boolean, plan_tier: Tier, limits: Plan, usage: { messages: number, voice_minutes: number, pages: number } }
Success: 200

POST /bots/{botId}/knowledge/ingest
Queue one URL/text document
Scope: kb:write
Request: { documents: [{ title: string(1..300), url?: public URL, text?: string(1..200000), source_type?: url | qa | snippet }] }; exactly one url/text; Idempotency-Key required
Response: { data: { training_id: UUID, status: queued, total: number } }
Success: 202

POST /bots/{botId}/knowledge/ingest-bulk
Queue up to 100 selected documents
Scope: kb:write
Request: { documents: DocumentInput[1..100] }; Idempotency-Key required
Response: { data: { training_id: UUID, status: queued, total: number } }
Success: 202

GET /bots/{botId}/knowledge
List knowledge documents and status
Scope: kb:read
Request: none
Response: { data: KnowledgeDocument[] }
Success: 200

GET /bots/{botId}/training/{trainingId}
Poll tracked training progress
Scope: kb:read
Request: none
Response: { training_id: UUID, total: number, ready: number, failed: number, status: processing | completed | partial_failure | failed, documents: KnowledgeDocument[], poll_after_seconds: 5 }
Success: 200

POST /bots/{botId}/knowledge/discover
Find website pages without starting training
Scope: kb:write
Request: { url: public URL, max_pages?: integer(1..100)=100, include_paths?: string[0..20], exclude_paths?: string[0..20] }
Response: { urls: URL[], pagesFetched: number, truncated: boolean, next_step: string }
Success: 200

POST /bots/{botId}/knowledge/upload
Upload PDF, DOCX, XLSX, XLS, TXT, CSV or Markdown (10 MiB maximum)
Scope: kb:write
Request: Raw file bytes; X-File-Name: percent-encoded filename; Idempotency-Key required
Response: { data: { training_id: UUID, status: queued, total: number } }
Success: 202

GET /bots/{botId}/knowledge/{documentId}
Read one document, its content and current status
Scope: kb:read
Request: none
Response: { data: KnowledgeDocument }
Success: 200

DELETE /bots/{botId}/knowledge/{documentId}
Delete one document and return its page usage
Scope: kb:write
Request: none
Response: { deleted: UUID }
Success: 200

PATCH /bots/{botId}/knowledge/{documentId}
Edit and retrain a QA/snippet source; queued documents keep their in-progress job
Scope: kb:write
Request: { title?: string(1..300), text: string(1..200000) }
Response: { data: { training_id: UUID, status: queued | processing, document_id: UUID } }
Success: 202

POST /bots/{botId}/knowledge/{documentId}/retrain
Retrain using the stored source; active jobs are reused
Scope: kb:write
Request: none
Response: { data: { training_id: UUID, status: queued | processing, document_id: UUID } }
Success: 202

GET /bots/{botId}/analytics
Read this bot’s analytics. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.
Scope: operations:read
Request: none
Response: { data: analytics[] | object, settings?: object }
Success: 200

GET /bots/{botId}/leads
Read this bot’s leads. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.
Scope: operations:read
Request: none
Response: { data: leads[] | object, settings?: object }
Success: 200

GET /bots/{botId}/conversations
Read this bot’s conversations. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.
Scope: operations:read
Request: none
Response: { data: conversations[] | object, settings?: object }
Success: 200

GET /bots/{botId}/tickets
Read this bot’s tickets. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.
Scope: operations:read
Request: none
Response: { data: tickets[] | object, settings?: object }
Success: 200

GET /bots/{botId}/contacts
Read this bot’s contacts. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.
Scope: operations:read
Request: none
Response: { data: contacts[] | object, settings?: object }
Success: 200

GET /bots/{botId}/bookings
Read this bot’s bookings. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.
Scope: operations:read
Request: none
Response: { data: bookings[] | object, settings?: object }
Success: 200

GET /bots/{botId}/summaries
Read this bot’s summaries. Bounded results: leads 500, conversations 100, tickets/contacts 300, bookings 200, summaries 12.
Scope: operations:read
Request: none
Response: { data: summaries[] | object, settings?: object }
Success: 200

GET /bots/{botId}/conversations/{sessionId}
Read up to 1,000 messages belonging to this bot and session
Scope: operations:read
Request: none
Response: { data: { id: UUID, role: string, content: string, created_at: ISODate, feedback: string | null }[] }
Success: 200

GET /bots/{botId}/inbox
Read up to 80 existing email inbox threads; does not purchase or enable an inbox
Scope: operations:read
Request: none
Response: { data: InboxThread[] }
Success: 200

GET /bots/{botId}/inbox/{id}
Read up to 1,000 messages in this bot’s email thread
Scope: operations:read
Request: none
Response: { data: Message[] }
Success: 200

PATCH /bots/{botId}/leads/{id}
Edit lead status and notes
Scope: operations:write
Request: { status?: new | contacted | qualified | won | lost, notes?: string(0..10000) }
Response: { data: Lead }
Success: 200

GET /bots/{botId}/tickets/{id}/messages
Read a ticket thread belonging to this bot
Scope: operations:read
Request: none
Response: { data: TicketMessage[] }
Success: 200

PATCH /bots/{botId}/tickets/{id}
Update ticket status
Scope: operations:write
Request: { status: open | in_progress | resolved | closed }
Response: { data: Ticket }
Success: 200

POST /bots/{botId}/tickets/{id}/messages
Reply as the enterprise support team; visible to the visitor
Scope: operations:write
Request: { body: string(1..10000) }
Response: { data: TicketMessage }
Success: 201

PATCH /bots/{botId}/bookings/{id}
Confirm/reject/reschedule/cancel using MonCha booking rules; vendor handles its own customer notifications
Scope: operations:write
Request: { action: confirm | reject | reschedule | cancel, requestedSlot?: ISODate, rejectionComment?: string(0..1000), cancelComment?: string(0..1000) }
Response: { data: Booking }
Success: 200

PATCH /bots/{botId}/summaries/settings
Set the bot’s summary cadence
Scope: operations:write
Request: { interval_days: 30 | 90 }
Response: { ok: true }
Success: 200

PUT /bots/{botId}/notifications
Set bot notification destinations; vendor overrides clear on disconnect/takeover
Scope: bots:write
Request: { notify_email: email | null, ticket_alert_emails: email[0..10] }
Response: { ok: true }
Success: 200

GET /bots/{botId}/functions
List only functions created through this vendor connection; secrets are omitted
Scope: integrations:manage
Request: none
Response: { data: Function[] }
Success: 200

DELETE /bots/{botId}/functions/{id}
Delete a vendor-owned functions resource
Scope: integrations:manage
Request: none
Response: { deleted: UUID }
Success: 200

GET /bots/{botId}/webhooks
List only webhooks created through this vendor connection; secrets are omitted
Scope: integrations:manage
Request: none
Response: { data: Webhook[] }
Success: 200

DELETE /bots/{botId}/webhooks/{id}
Delete a vendor-owned webhooks resource
Scope: integrations:manage
Request: none
Response: { deleted: UUID }
Success: 200

POST /bots/{botId}/functions
Create a bot-only function, subject to tier and enterprise API entitlement
Scope: integrations:manage
Request: { name: lowercase identifier(2..61), description: string(10..500), method: GET | POST, url: HTTPS URL, headers?: Record<string,string>, params: Record<string,string> }
Response: { data: Function }
Success: 201

POST /bots/{botId}/webhooks
Create a bot-only webhook; the signing secret is shown once; requires tier and contract webhooks
Scope: integrations:manage
Request: { url: HTTPS URL, events: (lead.created | conversation.handoff | message.unanswered | ticket.created | booking.created)[] }
Response: { data: Webhook & { secret: string } }
Success: 201

POST /bots/{botId}/integrations/{provider}/authorize
Start separate provider consent for notion or google; never uses customer-wide connections
Scope: integrations:manage
Request: { redirect_uri: registered URL, state: string(32..256) }
Response: { authorization_url: URL, expires_in: 600 }
Success: 201

GET /bots/{botId}/integrations/{provider}
List pages/files available through this bot/vendor’s provider grant
Scope: integrations:manage
Request: none
Response: { data: { id: string, title: string, url?: URL, mimeType?: string }[] }
Success: 200

DELETE /bots/{botId}/integrations/{provider}
Delete this scoped provider grant; imported documents are retained
Scope: integrations:manage
Request: none
Response: { disconnected: true }
Success: 200

POST /bots/{botId}/integrations/{provider}/import
Import up to 20 selected pages/files as editable text snapshots
Scope: integrations:manage + kb:write
Request: { items: { id: string, title: string(1..300), mime_type?: string }[1..20] }; Idempotency-Key required
Response: { data: { training_id: UUID, status: queued, total: number } }
Success: 202

GET /bots/{botId}/channels/whatsapp
Read this vendor’s bring-your-own WhatsApp connection
Scope: integrations:manage
Request: none
Response: { data: Channel | null }
Success: 200

POST /bots/{botId}/channels/whatsapp/connect
Start provider-hosted connection in a dedicated bot profile; does not buy numbers
Scope: integrations:manage
Request: { redirect_uri: registered URL }
Response: { authorization_url: URL }
Success: 200

POST /bots/{botId}/channels/whatsapp/complete
Complete after provider consent; retry 409 connect_incomplete after five seconds
Scope: integrations:manage
Request: none
Response: { data: { id: UUID, status: active, phone_number: string | null } }
Success: 200

PATCH /bots/{botId}/channels/whatsapp
Configure the vendor-owned channel
Scope: integrations:manage
Request: { aiEnabled?: boolean, showOnWidget?: boolean }
Response: { ok: true }
Success: 200

DELETE /bots/{botId}/channels/whatsapp
Disconnect the vendor-owned channel
Scope: integrations:manage
Request: none
Response: { disconnected: true }
Success: 200

ERRORS
Envelope: { error: { code: string, message: string }, request_id?: string }. Validation failures identify invalid fields.
401 invalid_vendor_credentials / vendor_tenant_mismatch. 403 vendor_disabled / insufficient_scope / insufficient_consent. 402 vendor_subscription_required. 404 customer_not_found / bot_not_found / training_not_found. 409 slot_unavailable / bot_name_taken / idempotency_conflict. 429 rate_limit_exceeded with Retry-After. 503 request_failed (retry with backoff).
Never retry 4xx unchanged. Retry transient 5xx with the SAME Idempotency-Key and body. Keys are 8–200 letters/digits/underscore/colon/dot/hyphen. Reusing a key with different content is a conflict. Bot and ingestion results are stored durably.
300 requests/minute per enterprise (best-effort KV limiter); apply backoff on 429. Rotate by creating a new key, updating your backend, then revoking the old key.

EXAMPLE
curl 'https://api.moncha.co/vendor/v1/customers/CONNECTION_ID/bots' \
 -H 'Authorization: Bearer YOUR_SERVER_SECRET' \
 -H 'X-Vendor-Tenant-Id: ENTERPRISE_TENANT_ID' \
 -H 'Idempotency-Key: customer-bot-001' \
 -H 'Content-Type: application/json' \
 -d '{"name":"Support","plan_tier":"growth","language":"en"}'

ACCOUNT BOUNDARIES
Personal billing, passwords, team membership, enterprise contract editing and customer-wide API tokens are not delegated. Paid phone-number/inbox/add-on purchases are not part of this vendor release. No training webhooks are advertised; use polling. Never assume an undocumented endpoint exists.

KNOWLEDGE
A YouTube watch URL can be submitted to knowledge/ingest as a URL document. QA uses source_type=qa, question as title and answer as text. Snippets use source_type=snippet. Bulk sources support scrape_options: {extractMainOnly?: boolean, includeSelectors?: string[0..30], excludeSelectors?: string[0..30]}; custom authorization/cookie headers are not accepted. Discover first, select returned URLs, then ingest-bulk. Retrain a whole bot by listing knowledge and calling each document’s retrain endpoint; retain each returned training_id and poll. Imports are snapshots; import again to refresh from the provider.

PROVIDER FLOW
For integrations/{provider}, provider is notion or google. Authorize returns the provider URL; open it in the browser. MonCha uses its existing configured provider callback. The vendor callback receives state and connected=provider or error=provider_connection_failed. Verify state, then list and import. No provider access token is returned. WhatsApp uses a dedicated bot profile: open connect’s authorization_url, then call complete from your backend. Existing customer channel connections cannot be replaced by vendor credentials.

WEBHOOKS
Events: lead.created, conversation.handoff, message.unanswered, ticket.created, booking.created. Payload: {event:string,timestamp:number,data:object}. Verify X-MonCha-Signature as lowercase hex HMAC-SHA256(secret, timestamp + '.' + rawBody), compare in constant time, check X-MonCha-Timestamp within five minutes, and deduplicate. Current delivery is best effort without durable retries. Revocation, payment loss and takeover suppress vendor-created hooks/functions.

SCHEMAS
{
  "Error": {
    "type": "object",
    "properties": {
      "error": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "message"
        ],
        "additionalProperties": false
      },
      "request_id": {
        "type": "string"
      }
    },
    "required": [
      "error"
    ],
    "additionalProperties": false
  },
  "Tier": {
    "type": "string",
    "enum": [
      "starter",
      "growth",
      "scale"
    ]
  },
  "Slot": {
    "type": "object",
    "properties": {
      "allocated": {
        "type": "number"
      },
      "used": {
        "type": "number"
      },
      "available": {
        "type": "number"
      }
    },
    "required": [
      "allocated",
      "used",
      "available"
    ],
    "additionalProperties": false
  },
  "Bot": {
    "type": "object",
    "properties": {
      "bot_id": {
        "type": "string",
        "format": "uuid"
      },
      "customer_tenant_id": {
        "type": "string",
        "format": "uuid"
      },
      "name": {
        "type": "string"
      },
      "plan_tier": {
        "$ref": "#/components/schemas/Tier"
      },
      "is_active": {
        "type": "boolean"
      },
      "vendor_deactivated_at": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      }
    },
    "required": [
      "bot_id",
      "customer_tenant_id",
      "name",
      "plan_tier",
      "is_active",
      "vendor_deactivated_at"
    ],
    "additionalProperties": false
  },
  "Customer": {
    "type": "object",
    "properties": {
      "connection_id": {
        "type": "string",
        "format": "uuid"
      },
      "customer_tenant_id": {
        "type": "string",
        "format": "uuid"
      },
      "external_customer_id": {
        "type": "string"
      },
      "connected_at": {
        "type": "string",
        "format": "date-time"
      },
      "name": {
        "type": "string"
      },
      "email": {
        "type": "string",
        "format": "email"
      }
    },
    "required": [
      "connection_id",
      "customer_tenant_id",
      "external_customer_id",
      "connected_at",
      "name",
      "email"
    ],
    "additionalProperties": false
  },
  "BotCreate": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "minLength": 1,
        "maxLength": 30
      },
      "plan_tier": {
        "$ref": "#/components/schemas/Tier"
      },
      "language": {
        "type": "string",
        "minLength": 2,
        "maxLength": 10,
        "default": "en"
      }
    },
    "required": [
      "name",
      "plan_tier"
    ],
    "additionalProperties": false
  },
  "BotSettings": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "modelTier": {
        "type": "string",
        "enum": [
          "fast",
          "quality"
        ]
      },
      "language": {
        "type": "string",
        "minLength": 2,
        "maxLength": 10
      },
      "languageMode": {
        "type": "string",
        "enum": [
          "match",
          "fixed"
        ]
      },
      "supportedLanguages": {
        "maxItems": 40,
        "type": "array",
        "items": {
          "type": "string",
          "minLength": 2,
          "maxLength": 10
        }
      },
      "fallbackMessage": {
        "type": "string",
        "maxLength": 500
      },
      "historyMessages": {
        "type": "integer",
        "minimum": 0,
        "maximum": 6
      },
      "instructions": {
        "type": "string",
        "maxLength": 4000
      },
      "instructionPreset": {
        "type": "string",
        "maxLength": 40
      },
      "persona": {
        "type": "string",
        "maxLength": 2000
      },
      "personaPreset": {
        "type": "string",
        "maxLength": 40
      },
      "temperature": {
        "type": "number",
        "minimum": 0,
        "maximum": 1
      },
      "collectUserData": {
        "type": "string",
        "enum": [
          "mandatory",
          "optional",
          "off"
        ]
      },
      "collectFields": {
        "maxItems": 3,
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "name",
            "email",
            "phone"
          ]
        }
      },
      "captureMode": {
        "type": "string",
        "enum": [
          "off",
          "passive",
          "conversational",
          "form"
        ]
      },
      "leadGoal": {
        "type": "string",
        "maxLength": 2000
      },
      "notifyThreshold": {
        "type": "integer",
        "minimum": 0,
        "maximum": 100
      },
      "translations": {
        "type": "object",
        "propertyNames": {
          "type": "string",
          "minLength": 2,
          "maxLength": 10
        },
        "additionalProperties": {
          "type": "object",
          "properties": {
            "fallbackMessage": {
              "type": "string",
              "maxLength": 500
            }
          },
          "additionalProperties": false
        }
      }
    },
    "additionalProperties": false
  },
  "WidgetConfig": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "botName": {
        "type": "string",
        "minLength": 1,
        "maxLength": 30
      },
      "tooltip": {
        "type": "string",
        "maxLength": 300
      },
      "welcomeMessage": {
        "type": "string",
        "maxLength": 500
      },
      "inputPlaceholder": {
        "type": "string",
        "maxLength": 80
      },
      "primaryColor": {
        "type": "string",
        "pattern": "^#[0-9a-fA-F]{6}$"
      },
      "textColor": {
        "type": "string",
        "pattern": "^#[0-9a-fA-F]{6}$"
      },
      "linkColor": {
        "type": "string",
        "pattern": "^#[0-9a-fA-F]{6}$"
      },
      "iconBgColor": {
        "type": "string",
        "pattern": "^#[0-9a-fA-F]{6}$"
      },
      "botAvatarBgColor": {
        "type": "string",
        "pattern": "^#[0-9a-fA-F]{6}$"
      },
      "voiceOrbColor": {
        "type": "string",
        "pattern": "^#[0-9a-fA-F]{6}$"
      },
      "transparentIconBg": {
        "type": "boolean"
      },
      "launcherIconUrl": {
        "type": "string",
        "format": "uri"
      },
      "botIconUrl": {
        "type": "string",
        "format": "uri"
      },
      "userIconUrl": {
        "type": "string",
        "format": "uri"
      },
      "agentIconUrl": {
        "type": "string",
        "format": "uri"
      },
      "showExpand": {
        "type": "boolean"
      },
      "fontSize": {
        "type": "integer",
        "minimum": 11,
        "maximum": 20
      },
      "chatHeightPct": {
        "type": "integer",
        "minimum": 40,
        "maximum": 100
      },
      "darkMode": {
        "type": "boolean"
      },
      "autoOpenDesktopSec": {
        "anyOf": [
          {
            "type": "integer",
            "minimum": 0,
            "maximum": 120
          },
          {
            "type": "null"
          }
        ]
      },
      "autoOpenMobileSec": {
        "anyOf": [
          {
            "type": "integer",
            "minimum": 0,
            "maximum": 120
          },
          {
            "type": "null"
          }
        ]
      },
      "requireTerms": {
        "type": "boolean"
      },
      "termsUrl": {
        "type": "string",
        "format": "uri"
      },
      "disclaimerText": {
        "type": "string",
        "maxLength": 500
      },
      "rtl": {
        "type": "boolean"
      },
      "externalLinkUrl": {
        "type": "string",
        "format": "uri"
      },
      "chatDisabled": {
        "type": "boolean"
      },
      "disabledMessage": {
        "type": "string",
        "maxLength": 300
      },
      "maxMessagesPerConversation": {
        "type": "integer",
        "minimum": 0,
        "maximum": 100
      },
      "showDownload": {
        "type": "boolean"
      },
      "iconSize": {
        "type": "string",
        "enum": [
          "small",
          "medium",
          "large"
        ]
      },
      "iconShape": {
        "type": "string",
        "enum": [
          "circle",
          "square",
          "rounded"
        ]
      },
      "position": {
        "type": "string",
        "enum": [
          "bottom-right",
          "bottom-left"
        ]
      },
      "offsetBottom": {
        "type": "integer",
        "minimum": 0,
        "maximum": 200
      },
      "offsetSide": {
        "type": "integer",
        "minimum": 0,
        "maximum": 200
      },
      "offsetBottomMobile": {
        "type": "integer",
        "minimum": 0,
        "maximum": 200
      },
      "offsetSideMobile": {
        "type": "integer",
        "minimum": 0,
        "maximum": 200
      },
      "launcherShade": {
        "type": "integer",
        "minimum": 0,
        "maximum": 100
      },
      "hideBranding": {
        "type": "boolean"
      },
      "hideSources": {
        "type": "boolean"
      },
      "hideFeedback": {
        "type": "boolean"
      },
      "hideRefresh": {
        "type": "boolean"
      },
      "hideTooltip": {
        "type": "boolean"
      },
      "voiceMode": {
        "type": "boolean"
      },
      "ticketDepartments": {
        "type": "object",
        "propertyNames": {
          "type": "string"
        },
        "additionalProperties": {
          "type": "boolean"
        }
      },
      "watermarkLink": {
        "type": "string",
        "format": "uri"
      },
      "watermarkIconUrl": {
        "type": "string",
        "format": "uri"
      },
      "quickPrompts": {
        "maxItems": 6,
        "type": "array",
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        }
      },
      "followUpChips": {
        "type": "boolean"
      },
      "followUpPrompts": {
        "maxItems": 3,
        "type": "array",
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 120
        }
      },
      "strings": {
        "type": "object",
        "properties": {
          "online": {
            "type": "string",
            "maxLength": 40
          },
          "welcome": {
            "type": "string",
            "maxLength": 500
          },
          "placeholder": {
            "type": "string",
            "maxLength": 80
          },
          "errorGeneric": {
            "type": "string",
            "maxLength": 200
          },
          "errorNoResponse": {
            "type": "string",
            "maxLength": 200
          },
          "poweredBy": {
            "type": "string",
            "maxLength": 60
          },
          "openChat": {
            "type": "string",
            "maxLength": 60
          },
          "closeChat": {
            "type": "string",
            "maxLength": 60
          },
          "send": {
            "type": "string",
            "maxLength": 40
          },
          "startVoice": {
            "type": "string",
            "maxLength": 80
          },
          "stopVoice": {
            "type": "string",
            "maxLength": 80
          },
          "recording": {
            "type": "string",
            "maxLength": 40
          },
          "talkToHuman": {
            "type": "string",
            "maxLength": 60
          },
          "handoffSent": {
            "type": "string",
            "maxLength": 200
          },
          "handoffFailed": {
            "type": "string",
            "maxLength": 200
          },
          "fieldName": {
            "type": "string",
            "maxLength": 40
          },
          "fieldEmail": {
            "type": "string",
            "maxLength": 40
          },
          "fieldPhone": {
            "type": "string",
            "maxLength": 40
          },
          "submit": {
            "type": "string",
            "maxLength": 40
          },
          "skip": {
            "type": "string",
            "maxLength": 40
          },
          "leadThanks": {
            "type": "string",
            "maxLength": 200
          }
        },
        "additionalProperties": false
      },
      "bookingEnabled": {
        "type": "boolean"
      },
      "bookingConfig": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 60
          },
          "requireNotes": {
            "type": "boolean"
          },
          "extraFields": {
            "maxItems": 5,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "maxLength": 40
                },
                "label": {
                  "type": "string",
                  "maxLength": 60
                },
                "required": {
                  "type": "boolean"
                }
              },
              "required": [
                "key",
                "label",
                "required"
              ],
              "additionalProperties": false
            }
          },
          "hoursStart": {
            "type": "string",
            "pattern": "^\\d{2}:\\d{2}$"
          },
          "hoursEnd": {
            "type": "string",
            "pattern": "^\\d{2}:\\d{2}$"
          },
          "slotMinutes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1440
          },
          "visitorPicksTime": {
            "type": "boolean"
          },
          "mapsUrl": {
            "type": "string",
            "maxLength": 2000
          }
        },
        "additionalProperties": false
      },
      "homeChannels": {
        "maxItems": 1,
        "type": "array",
        "items": {}
      },
      "translations": {
        "type": "object",
        "propertyNames": {
          "type": "string",
          "minLength": 2,
          "maxLength": 10
        },
        "additionalProperties": {
          "type": "object",
          "properties": {
            "welcomeMessage": {
              "type": "string",
              "maxLength": 500
            },
            "tooltip": {
              "type": "string",
              "maxLength": 300
            },
            "quickPrompts": {
              "maxItems": 6,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              }
            }
          },
          "additionalProperties": false
        }
      }
    },
    "additionalProperties": false
  },
  "Training": {
    "type": "object",
    "properties": {
      "training_id": {
        "type": "string",
        "format": "uuid"
      },
      "status": {
        "type": "string",
        "enum": [
          "queued",
          "processing",
          "completed",
          "partial_failure",
          "failed"
        ]
      },
      "total": {
        "type": "integer"
      },
      "ready": {
        "type": "integer"
      },
      "failed": {
        "type": "integer"
      },
      "document_id": {
        "type": "string",
        "format": "uuid"
      },
      "documents": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/KnowledgeDocument"
        }
      },
      "poll_after_seconds": {
        "type": "integer"
      }
    },
    "required": [
      "training_id",
      "status"
    ],
    "additionalProperties": false
  },
  "KnowledgeDocument": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "title": {
        "type": "string"
      },
      "source_url": {
        "type": [
          "string",
          "null"
        ]
      },
      "source_type": {
        "type": "string"
      },
      "content": {
        "type": [
          "string",
          "null"
        ]
      },
      "status": {
        "type": "string",
        "enum": [
          "queued",
          "processing",
          "ready",
          "failed"
        ]
      },
      "chunk_count": {
        "type": "integer"
      },
      "pages_count": {
        "type": "integer"
      },
      "synced_at": {
        "type": [
          "string",
          "null"
        ]
      },
      "error_message": {
        "type": [
          "string",
          "null"
        ]
      }
    },
    "required": [
      "id",
      "title",
      "status",
      "chunk_count",
      "pages_count"
    ],
    "additionalProperties": false
  },
  "DocumentInput": {
    "type": "object",
    "properties": {
      "title": {
        "type": "string",
        "minLength": 1,
        "maxLength": 300
      },
      "url": {
        "type": "string",
        "format": "uri",
        "maxLength": 2048
      },
      "text": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200000
      },
      "source_type": {
        "type": "string",
        "enum": [
          "url",
          "qa",
          "snippet"
        ],
        "default": "url"
      },
      "scrape_options": {
        "type": "object",
        "properties": {
          "extractMainOnly": {
            "type": "boolean"
          },
          "includeSelectors": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 200
            },
            "maxItems": 30
          },
          "excludeSelectors": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 200
            },
            "maxItems": 30
          }
        },
        "required": [],
        "additionalProperties": false
      }
    },
    "required": [
      "title"
    ],
    "additionalProperties": false,
    "oneOf": [
      {
        "required": [
          "url"
        ],
        "not": {
          "required": [
            "text"
          ]
        }
      },
      {
        "required": [
          "text"
        ],
        "not": {
          "required": [
            "url"
          ]
        }
      }
    ]
  },
  "TicketMessage": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "ticket_id": {
        "type": "string",
        "format": "uuid"
      },
      "tenant_id": {
        "type": "string",
        "format": "uuid"
      },
      "author_type": {
        "type": "string"
      },
      "author_id": {
        "type": [
          "string",
          "null"
        ]
      },
      "author_name": {
        "type": [
          "string",
          "null"
        ]
      },
      "body": {
        "type": "string"
      },
      "attachment_url": {
        "type": [
          "string",
          "null"
        ]
      },
      "attachment_type": {
        "type": [
          "string",
          "null"
        ]
      },
      "created_at": {
        "type": "string"
      }
    },
    "required": [
      "id",
      "ticket_id",
      "tenant_id",
      "author_type",
      "author_id",
      "author_name",
      "body",
      "attachment_url",
      "attachment_type",
      "created_at"
    ],
    "additionalProperties": false
  },
  "Message": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "role": {
        "type": "string"
      },
      "content": {
        "type": "string"
      },
      "created_at": {
        "type": "string",
        "format": "date-time"
      },
      "feedback": {
        "type": [
          "string",
          "null"
        ]
      }
    },
    "required": [
      "id",
      "role",
      "content",
      "created_at",
      "feedback"
    ],
    "additionalProperties": false
  },
  "Conversation": {
    "type": "object",
    "properties": {
      "session_id": {
        "type": "string"
      },
      "message_count": {
        "type": "number"
      },
      "last_at": {
        "type": "string",
        "format": "date-time"
      }
    },
    "required": [
      "session_id",
      "message_count",
      "last_at"
    ],
    "additionalProperties": false
  },
  "Contact": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "name": {
        "type": [
          "string",
          "null"
        ]
      },
      "email": {
        "type": [
          "string",
          "null"
        ]
      },
      "is_guest": {
        "type": "boolean"
      },
      "created_at": {
        "type": "string",
        "format": "date-time"
      }
    },
    "required": [
      "id",
      "name",
      "email",
      "is_guest",
      "created_at"
    ],
    "additionalProperties": false
  },
  "Lead": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "tenant_id": {
        "type": "string",
        "format": "uuid"
      },
      "chatbot_id": {
        "type": [
          "string",
          "null"
        ]
      },
      "session_id": {
        "type": "string"
      },
      "name": {
        "type": [
          "string",
          "null"
        ]
      },
      "email": {
        "type": [
          "string",
          "null"
        ]
      },
      "phone": {
        "type": [
          "string",
          "null"
        ]
      },
      "external_user_id": {
        "type": [
          "string",
          "null"
        ]
      },
      "company": {
        "type": [
          "string",
          "null"
        ]
      },
      "intent_category": {
        "type": [
          "string",
          "null"
        ]
      },
      "intent_summary": {
        "type": [
          "string",
          "null"
        ]
      },
      "sentiment": {
        "type": [
          "string",
          "null"
        ]
      },
      "notified_at": {
        "type": [
          "string",
          "null"
        ]
      },
      "asked_contact_at": {
        "type": [
          "string",
          "null"
        ]
      },
      "notes": {
        "type": [
          "string",
          "null"
        ]
      },
      "country": {
        "type": [
          "string",
          "null"
        ]
      },
      "region": {
        "type": [
          "string",
          "null"
        ]
      },
      "score": {
        "type": "number"
      },
      "score_band": {
        "type": "string",
        "enum": [
          "hot",
          "warm",
          "cold"
        ]
      },
      "qualification": {
        "type": [
          "object",
          "null"
        ],
        "additionalProperties": {
          "type": "string"
        }
      },
      "status": {
        "type": "string",
        "enum": [
          "new",
          "contacted",
          "qualified",
          "won",
          "lost"
        ]
      },
      "captured_via": {
        "type": "string"
      },
      "confidence": {
        "type": [
          "number",
          "null"
        ]
      },
      "channel": {
        "type": "string"
      },
      "created_at": {
        "type": "string",
        "format": "date-time"
      },
      "updated_at": {
        "type": "string",
        "format": "date-time"
      }
    },
    "required": [
      "id",
      "tenant_id",
      "chatbot_id",
      "session_id",
      "name",
      "email",
      "phone",
      "external_user_id",
      "company",
      "intent_category",
      "intent_summary",
      "sentiment",
      "notified_at",
      "asked_contact_at",
      "notes",
      "country",
      "region",
      "score",
      "score_band",
      "qualification",
      "status",
      "captured_via",
      "confidence",
      "channel",
      "created_at",
      "updated_at"
    ],
    "additionalProperties": true
  },
  "Ticket": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "tenant_id": {
        "type": "string",
        "format": "uuid"
      },
      "contact_id": {
        "type": "string",
        "format": "uuid"
      },
      "chatbot_id": {
        "type": [
          "string",
          "null"
        ]
      },
      "session_id": {
        "type": [
          "string",
          "null"
        ]
      },
      "subject": {
        "type": "string"
      },
      "department": {
        "type": [
          "string",
          "null"
        ]
      },
      "status": {
        "type": "string",
        "enum": [
          "open",
          "in_progress",
          "resolved",
          "closed"
        ]
      },
      "priority": {
        "type": "string"
      },
      "assigned_to": {
        "type": [
          "string",
          "null"
        ]
      },
      "agent_unread": {
        "type": "boolean"
      },
      "visitor_unread": {
        "type": "boolean"
      },
      "created_at": {
        "type": "string",
        "format": "date-time"
      },
      "updated_at": {
        "type": "string",
        "format": "date-time"
      },
      "last_message_at": {
        "type": "string",
        "format": "date-time"
      }
    },
    "required": [
      "id",
      "tenant_id",
      "contact_id",
      "chatbot_id",
      "session_id",
      "subject",
      "department",
      "status",
      "priority",
      "assigned_to",
      "agent_unread",
      "visitor_unread",
      "created_at",
      "updated_at",
      "last_message_at"
    ],
    "additionalProperties": true,
    "description": "List responses additionally include contact_name and contact_email."
  },
  "Booking": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "tenant_id": {
        "type": "string",
        "format": "uuid"
      },
      "chatbot_id": {
        "type": [
          "string",
          "null"
        ]
      },
      "name": {
        "type": "string"
      },
      "email": {
        "type": "string"
      },
      "phone": {
        "type": "string"
      },
      "notes": {
        "type": [
          "string",
          "null"
        ]
      },
      "requested_slot": {
        "type": [
          "string",
          "null"
        ]
      },
      "country": {
        "type": [
          "string",
          "null"
        ]
      },
      "time_zone": {
        "type": [
          "string",
          "null"
        ]
      },
      "rejection_comment": {
        "type": [
          "string",
          "null"
        ]
      },
      "confirmed_at": {
        "type": [
          "string",
          "null"
        ]
      },
      "rejected_at": {
        "type": [
          "string",
          "null"
        ]
      },
      "cancelled_at": {
        "type": [
          "string",
          "null"
        ]
      },
      "custom_fields": {
        "type": [
          "object",
          "null"
        ],
        "additionalProperties": {
          "type": "string"
        }
      },
      "status": {
        "type": "string",
        "enum": [
          "pending",
          "confirmed",
          "rejected",
          "cancelled"
        ]
      },
      "created_at": {
        "type": "string",
        "format": "date-time"
      }
    },
    "required": [
      "id",
      "tenant_id",
      "chatbot_id",
      "name",
      "email",
      "phone",
      "notes",
      "requested_slot",
      "country",
      "time_zone",
      "rejection_comment",
      "confirmed_at",
      "rejected_at",
      "cancelled_at",
      "custom_fields",
      "status",
      "created_at"
    ],
    "additionalProperties": true
  },
  "Function": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "name": {
        "type": "string"
      },
      "description": {
        "type": "string"
      },
      "method": {
        "type": "string"
      },
      "url": {
        "type": "string"
      },
      "params": {
        "type": "object",
        "additionalProperties": {
          "type": "string"
        }
      },
      "is_active": {
        "type": "boolean"
      },
      "created_at": {
        "type": "string",
        "format": "date-time"
      },
      "tenant_id": {
        "type": "string",
        "format": "uuid"
      },
      "chatbot_id": {
        "type": [
          "string",
          "null"
        ]
      },
      "vendor_connection_id": {
        "type": [
          "string",
          "null"
        ]
      }
    },
    "required": [
      "id",
      "name",
      "description",
      "method",
      "url",
      "params",
      "is_active",
      "created_at"
    ],
    "additionalProperties": false
  },
  "Webhook": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "url": {
        "type": "string"
      },
      "events": {
        "type": "array",
        "items": {
          "type": "string"
        }
      },
      "is_active": {
        "type": "boolean"
      },
      "last_status": {
        "type": [
          "number",
          "null"
        ]
      },
      "last_triggered_at": {
        "type": [
          "string",
          "null"
        ]
      },
      "created_at": {
        "type": "string",
        "format": "date-time"
      },
      "secret": {
        "type": "string"
      },
      "tenant_id": {
        "type": "string",
        "format": "uuid"
      },
      "chatbot_id": {
        "type": [
          "string",
          "null"
        ]
      },
      "vendor_connection_id": {
        "type": [
          "string",
          "null"
        ]
      }
    },
    "required": [
      "id",
      "url",
      "events",
      "is_active",
      "last_status",
      "last_triggered_at",
      "created_at"
    ],
    "additionalProperties": false
  },
  "Channel": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "phone_number": {
        "type": [
          "string",
          "null"
        ]
      },
      "display_name": {
        "type": [
          "string",
          "null"
        ]
      },
      "status": {
        "type": "string"
      },
      "ai_enabled": {
        "type": "boolean"
      },
      "show_on_widget": {
        "type": "boolean"
      }
    },
    "required": [
      "id",
      "phone_number",
      "display_name",
      "status",
      "ai_enabled",
      "show_on_widget"
    ],
    "additionalProperties": false
  },
  "InboxThread": {
    "type": "object",
    "properties": {
      "conversation_id": {
        "type": "string",
        "format": "uuid"
      },
      "session_id": {
        "type": "string"
      },
      "subject": {
        "type": "string"
      },
      "visitor": {
        "type": "string"
      },
      "last_at": {
        "type": "string",
        "format": "date-time"
      },
      "inbound_count": {
        "type": "number"
      },
      "outbound_count": {
        "type": "number"
      }
    },
    "required": [
      "conversation_id",
      "session_id",
      "subject",
      "visitor",
      "last_at",
      "inbound_count",
      "outbound_count"
    ],
    "additionalProperties": false
  },
  "Analytics": {
    "type": "object",
    "properties": {
      "daily": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "day": {
              "type": "string"
            },
            "user_messages": {
              "type": "number"
            },
            "assistant_messages": {
              "type": "number"
            }
          },
          "required": [
            "day",
            "user_messages",
            "assistant_messages"
          ],
          "additionalProperties": false
        }
      },
      "top_questions": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "question": {
              "type": "string"
            },
            "times": {
              "type": "number"
            }
          },
          "required": [
            "question",
            "times"
          ],
          "additionalProperties": false
        }
      },
      "unanswered": {
        "type": "object",
        "properties": {
          "count": {
            "type": "number"
          },
          "total": {
            "type": "number"
          },
          "rate": {
            "type": "number"
          }
        },
        "required": [
          "count",
          "total",
          "rate"
        ],
        "additionalProperties": false
      },
      "feedback": {
        "type": "object",
        "properties": {
          "positive": {
            "type": "number"
          },
          "negative": {
            "type": "number"
          }
        },
        "required": [
          "positive",
          "negative"
        ],
        "additionalProperties": false
      },
      "sessions": {
        "type": "object",
        "properties": {
          "total": {
            "type": "number"
          },
          "avg_messages": {
            "type": "number"
          }
        },
        "required": [
          "total",
          "avg_messages"
        ],
        "additionalProperties": false
      }
    },
    "required": [
      "daily",
      "top_questions",
      "unanswered",
      "feedback",
      "sessions"
    ],
    "additionalProperties": false
  },
  "Summary": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "format": "uuid"
      },
      "period_start": {
        "type": "string",
        "format": "date-time"
      },
      "period_end": {
        "type": "string",
        "format": "date-time"
      },
      "message_count": {
        "type": "number"
      },
      "session_count": {
        "type": "number"
      },
      "insights": {
        "type": "object",
        "properties": {
          "topQuestions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "praise": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "topQuestions",
          "problems",
          "praise"
        ],
        "additionalProperties": false
      }
    },
    "required": [
      "id",
      "period_start",
      "period_end",
      "message_count",
      "session_count",
      "insights"
    ],
    "additionalProperties": false
  },
  "SummarySettings": {
    "anyOf": [
      {
        "type": "object",
        "properties": {
          "intervalDays": {
            "type": "integer",
            "enum": [
              30,
              90
            ]
          },
          "nextDueAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastSyncedAt": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "intervalDays",
          "nextDueAt",
          "lastSyncedAt"
        ],
        "additionalProperties": false
      },
      {
        "type": "null"
      }
    ]
  },
  "Plan": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string"
      },
      "message_limit": {
        "type": "number"
      },
      "pages_limit": {
        "type": "number"
      },
      "voice_minutes_limit": {
        "type": "number"
      },
      "api_access": {
        "type": "boolean"
      },
      "webhooks": {
        "type": "boolean"
      },
      "auto_sync_interval": {
        "type": [
          "string",
          "null"
        ]
      }
    },
    "additionalProperties": true,
    "description": "Resolved bot tier catalog with enterprise feature restrictions; -1 means unlimited."
  }
}

SERVER JAVASCRIPT EXAMPLE (Node.js; adapt to your backend session store)
import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
const base = 'https://api.moncha.co/vendor/v1';
async function moncha(path, body, key) {
  const r = await fetch(base + path, { method: body ? 'POST' : 'GET', headers: {
    Authorization: 'Bearer ' + process.env.MONCHA_VENDOR_API_KEY,
    'X-Vendor-Tenant-Id': process.env.MONCHA_VENDOR_TENANT_ID,
    'Content-Type': 'application/json', ...(key ? {'Idempotency-Key': key} : {})
  }, ...(body ? {body: JSON.stringify(body)} : {}) });
  const result = await r.json(); if (!r.ok) throw new Error(result.error?.code); return result;
}
// Start: use your authenticated local user; never accept an arbitrary customer ID from the browser.
const verifier = randomBytes(32).toString('base64url');
const state = randomBytes(32).toString('hex');
const challenge = createHash('sha256').update(verifier).digest('base64url');
// Store {state, verifier, localUserId, expiresAt} in your server-side session.
const session = await moncha('/connect/sessions', {external_customer_id: localUserId, redirect_uri: CALLBACK, state, code_challenge: challenge});
// Return only session.authorization_url to your frontend.
// Callback: require the same signed-in local session; check state using timingSafeEqual and delete it after use.
const connection = await moncha('/connect/exchange', {code: callbackCode, code_verifier: stored.verifier, redirect_uri: CALLBACK});
// Persist connection.connection_id under stored.localUserId.
const bot = await moncha('/customers/' + connection.connection_id + '/bots', {name:'Support',plan_tier:'growth'}, 'create-support-v1');
const training = await moncha('/bots/' + bot.data.bot_id + '/knowledge/ingest', {documents:[{title:'Website',url:'https://example.com'}]}, 'train-website-v1');
const progress = await moncha('/bots/' + bot.data.bot_id + '/training/' + training.data.training_id);
// Poll at poll_after_seconds until completed/partial_failure/failed.
// Use bot.data.install.tenantId and .chatbotId in the existing MonCha widget installation.

BROWSER EXAMPLE (no MonCha secret)
// Call from the Connect button click so browsers allow the popup.
const popup = window.open('about:blank', 'moncha-connect', 'width=520,height=760');
const response = await fetch('/your-backend/moncha/connect', {method:'POST'});
const {authorization_url} = await response.json();
if (popup) popup.location = authorization_url; else window.location.assign(authorization_url);
// Your callback page may close this popup after its backend completes exchange.
// Poll your own backend connection status; do not use wildcard postMessage origins.


CALLBACK VALIDATION (server-side; run BEFORE exchanging the code)
// stored is read from the same authenticated backend session used at connect.
const received = Buffer.from(callbackState || '', 'utf8');
const expected = Buffer.from(stored.state, 'utf8');
if (stored.localUserId !== authenticatedLocalUser.id || stored.expiresAt <= Date.now()
    || received.length !== expected.length || !timingSafeEqual(received, expected)) {
  throw new Error('Invalid or expired MonCha callback');
}
// Atomically consume this backend session's connect attempt. Reject duplicates.
// An access_denied callback ends this attempt without calling exchange.
// Never log the callback code, verifier, MonCha key or authorization session URL.

RESPONSE EXAMPLES
201 bot creation:
{"data":{"bot_id":"11111111-1111-4111-8111-111111111111","customer_tenant_id":"22222222-2222-4222-8222-222222222222","connection_id":"33333333-3333-4333-8333-333333333333","plan_tier":"growth","funding":"vendor","install":{"tenantId":"22222222-2222-4222-8222-222222222222","chatbotId":"11111111-1111-4111-8111-111111111111"}}}
202 training accepted:
{"data":{"training_id":"44444444-4444-4444-8444-444444444444","status":"queued","total":1}}
409 no available tier slot:
{"error":{"code":"slot_unavailable","message":"slot unavailable"}}
401 missing or wrong credential type:
{"error":{"code":"invalid_vendor_credentials","message":"invalid_vendor_credentials"},"request_id":"example"}

PERSONAL PAYMENT
The vendor does not initiate customer billing. Direct your customer to their MonCha dashboard's Sponsored bots page. MonCha shows a funding preview and uses the customer's normal authenticated checkout. Existing paid customers first use an available plan/add-on slot; if none is available, purchase an extra bot in Billing and retry Continue with my plan. The payment redirect alone does not activate the bot. Verified Stripe state and the recorded funding intent control finalization. Refresh payment status if a webhook is delayed. No vendor key can access these personal billing endpoints.

Related guides

Something unclear? Tell us.

Start free trial