API Reference

MyLiveChat REST API v1

Token-authenticated, JSON-only. Rate-limited at 60 requests per minute per token. Every response carries an X-MLC-Request-Id header you can quote when contacting support.

What's new

Recent additions to v1. Anything not listed here was already in the API at the prior dashboard release. The full machine-readable catalog is at /v1/api.ashx?resource=resources — useful for diffing across deployments.

EndpointVerb(s)What it does
heartbeatGETUnauthenticated liveness probe; bypasses rate limit. Designed for UptimeRobot / Pingdom / status.io polling without burning a real token's budget.
resources (alias capabilities)GETSelf-describing endpoint catalog — every resource + supported verbs + one-line summary. Stable shape so SDK generators can diff across releases.
healthGETNow also returns webhook_queue_pending + webhook_failed_24h so external monitors can alert on broken integration deliveries.
tickets / ticketPOST / PATCHCreate from scratch (auto-assign + optional initial_comment); mutate status / priority / assignee atomically.
ticket_replyPOSTPublic reply or internal note. Fires the same ticket.replied webhook + analytics events as a dashboard reply.
ticket_mergePOSTMerge source into target; comments + activity move atomically; emits ticket.merged.
tickets_bulkPOSTApply one action (resolve/close/reopen/assign/priority/add_tag) across ≤200 tickets; per-row report.
ticket_snoozePOST/DELETESuppress a ticket until ISO timestamp / minutes / "2h"-style relative shorthand. Idempotent unsnooze.
time_logPOSTAppend a time-tracking entry against a ticket. Append-only (corrections via negative minutes).
articles / articlePOST / PATCHFull KB article CRUD with publish-on-create.
kb_categories (alias categories)GETLive category catalog with article counts.
macrosPOST/PATCH/DELETEShared canned-reply macros. API-issued macros are always shared (token has no agent identity).
saved_viewsPOST/PATCH/DELETEShared ticket saved-views. Same shared-only discipline as macros.
analytics + analytics_*GETHeadline counters, per-day series, top zero-result KB queries, agent scorecard, resolution time by priority, channel breakdown.
sla_breachesGETLive-computed breach/warning open tickets, sorted by worst-overdue. Drives external on-call paging.
webhook_deliveries (alias deliveries)GETTenant-wide delivery log across all subscriptions, with by_status aggregate over the full window.
mentionsGETPer-agent unread @-mention list. Read companion to the ticket.mentioned webhook.
customerGETPer-customer rollup keyed on email or phone. Includes channel mix + ticket history.
audit_logGETCaller-tunable limit (default 500, max 1000) so CI ingest pipelines can fetch just the latest few events. Now also supports format=csv.
agentsGETRotation roster + skills. Lists the operational agent set (auto-assignment rotation) with each agent's declared skills inline + the next-in-line cursor. Drives setup wizards and partner UIs that need agent identities + skill profiles without two round-trips. CSV export supported (skills joined with ;).
business_calendarGET / PATCH / POST / DELETESLA business calendar (Section 8 v1). Per-tenant weekly hours + fixed-date holidays the SLA engine uses to compute first-response and resolution targets when sla.use_business_calendar is on (default OFF for backwards compat). Sibling resource business_calendar_holiday handles add/remove. Defaults to Mon–Fri 09:00–17:00 UTC when unset. Wall-clock SLA semantics for tenants who don't opt in remain byte-identical.
migrate_statusGETCutover progress in one call. Returns per-asset (tickets / articles / macros) cumulative {imported_rows, skipped_rows, failed_rows, run_count, last_run_utc} across all three importer audit tables. Drives partner CI tools monitoring a migration without three separate round-trips. Mirrors the data shown on the dashboard's /dashboard/migrate.ascx hub.
tickets + articles + audit_log + macros + tags + saved_views + kb_categories + webhook_subscriptions + webhook_deliveries + feature_flags + business_calendar + agents + sla_policy + visitors + csat + feedback + customer_identities + ai_usageGETCSV export: pass format=csv to any of these to receive the same row set (post-filter, post-paging) as text/csv with a timestamped Content-Disposition filename and RFC 4180 escaping. Compliance / SIEM / spreadsheet pipelines can ingest with one curl call. Block 213 added macros CSV — column shape matches the generic-CSV format macros_import consumes, so an export from one tenant re-imports cleanly into another. Block 216 added tags CSV — drives the spreadsheet-based "merge unused tags" cleanup workflow via applied_count. Block 218 added saved_views CSV for backup / replication into sibling tenants.
tickets + articles + macros + saved_viewsGETList responses now carry an applied_filters echo of active query params + paging cursor. Lets clients build "next page" links without tracking filter state on their side.
tagsGETEach row now includes applied_count — live count of confirmed applications across ConversationTagApplied. Drives "merge unused tags" cleanup tools without a follow-up join.
healthGETNow also includes last_inbound_message_utc — timestamp of the most recent inbound chat message across all sessions. Detects silent downtime where ticket counts look fine but no traffic is landing.
feature_flags (alias flags)GET / PATCHPer-tenant feature-flag system. Controlled rollout for any flagged feature; each flag has a system-wide default plus an optional tenant override. AppDomain-cached for 60s. Backed by dbo.FeatureFlag + FeatureFlagRepository. Initial catalog: calls.voice, calls.video, tickets.bulk_ui, tickets.merge_ui, kb.ai_search, ai.copilot, webhooks.token_events.
agent_skills, ticket_skills, skillsGET / PATCHSkills-based routing. Agents declare free-text skills; tickets declare required skills. Auto-assign rotation prefers agents who cover every required skill; falls back to round-robin if none match. POST /v1/tickets accepts a new required_skills array that drives the routing decision and is persisted to dbo.TicketSkill.
tickets_importPOSTBulk-migrate from Zendesk / Freshdesk / LiveAgent. Accepts CSV bodies (up to 16 MB) with auto-detect from header columns or explicit ?platform=. ?dry_run=1 returns counts + 10-row sample preview without writing. Idempotent on (site_id, platform, external_id) — re-running the same export is a no-op for previously-imported rows.
kb_importPOSTBulk-migrate KB articles from Zendesk Guide / Freshdesk Solutions / generic title-body CSVs. Same shape as tickets_import: dry-run preview, then commit. New articles land as drafts so admins can review before publishing. Each successful insert fires an article.created webhook for search-index sync subscribers.
macros_importPOSTBulk-migrate canned-reply macros from Zendesk Macros / Freshdesk Canned Responses / generic title-body CSVs. Completes the 3-importer set (tickets / KB / macros) — partners migrating tenants off competitors now have one curl call per asset class. Imported macros land as shared (tenant-wide) since exports rarely encode per-agent ownership.
feature_flags (csat.auto_issue_on_resolve)GET / PATCHCSAT auto-issue toggle. When ON (default), resolving a ticket issues a CSAT magic-link token and fires ticket.csat_issued. Tenants on Delighted / Wootric / their own pipeline can flip OFF to avoid double-surveying. The ticket-detail page now also renders a CSAT badge inline (★★★★☆ + comment) when a response landed.
customer.identity (Section 2)data modelSingle source of truth for "who is this person". New dbo.CustomerIdentity table keyed on (SiteId, Email) and (SiteId, Phone); dbo.Ticket.CustomerIdentityId column links every new ticket to the identity row at create time (Block 251). One-shot backfill script links pre-existing tickets (Block 250). The customer endpoint now returns the identity row inline so partner SDKs can key off the stable identity id. Foundation for cross-channel customer history once Section 5 channel integrations land.
portal_token + portal pagesPOST / DELETE / GET (HTML)Customer-facing portal end-to-end. Magic-link token API (portal_token); unauthenticated landing pages (/portal/index.aspx for ticket list, /portal/ticket.aspx for thread + reply composer); reply path (/portal/reply.ashx posts as requester). Cross-requester safety enforced on every read. Block 248: opt-in portal.auto_issue_on_ticket_create flag mints a token when a new ticket lands with a requester email — combined with the email-automation webhook subscription, every new requester receives their portal link automatically. Agent-side "Send portal link" button on the ticket-detail page. Token issuance always idempotent (reuses active tokens for the same email).
ticket.csat_responded, ticket.csat_low_scorewebhookCSAT response events. Every CSAT submission fires ticket.csat_responded (CRM enrichment, BI ingest). Submissions with score 1 or 2 also fire ticket.csat_low_score for detractor-only paging — and (when csat.low_score_auto_followup is ON, default) auto-apply the csat_followup tag so detractor feedback shows up in the standard triage queues without filter logic on the subscriber side.
saved_views (starter presets)auto-seededStarter saved-view chips. First visit to /dashboard/tickets.ascx on a fresh tenant lazily seeds four shared, pinned saved-views: "Detractor follow-up" (the csat_followup queue), "Unassigned", "Urgent open", and "Awaiting customer". Idempotent on title — existing customized rows are left untouched. New tenants land on a useful tickets list instead of an empty filter bar; veterans see no change.
csatGETNow accepts score_min + score_max (inclusive 1..5 bounds) and recent_window_days (independent window for the recent array). Detractor-only feed: ?score_max=2&recent_window_days=1&limit=200. Promoter-only / testimonial mining: ?score_min=5. Summary always covers the full days window regardless of recent filters. Block 212: opt-in ?include=trend adds a per-day series of {date, response_count, avg_score} across the active window — drives partner-side trend charts without a second round-trip. The same series powers the new sparkline in the dashboard's CSAT card.
channel.foundation (Section 2)data modelSchema for non-web channels. Additive dbo.Ticket columns ExternalThreadId / ExternalUserId / LastInboundUtc; new dbo.ChannelMessageVolume rollup; new dbo.CustomerIdentityChannel junction (one identity → N channel handles). Filtered hot-path index IX_Ticket_ChannelThread keeps webhook lookups under 1ms at scale. Apply via App_Data/channel_foundation_deploy.sql. Foundation for every Section 5 channel build (WhatsApp, Messenger, Instagram, SMS, Email-to-ticket).
bind_channel (alias channel_bind, identity_channel)POSTBind external channel handles to a customer identity. Idempotent — calling twice with the same triple refreshes last_seen_utc. Resolves identity by id → email → phone → mint. Whitelisted channels: whatsapp, messenger, instagram, sms, email. Use cases: partner pre-populates handles before launch (first inbound webhook threads into known identity instead of orphan), agent stitching ("this WhatsApp number is the same person as this email"), bulk import after migrating from a platform with separate handle storage. Fires identity.channel_bound webhook.
customerGETResponse now includes identity.channels[] array — every (channel, external_user_id, display_handle, first_seen_utc, last_seen_utc) bound to the identity. Partner SDKs key off this for "which channels can I reach this customer on?" without a follow-up call.
identity.channel_boundwebhookIdentity binding event. Fires from POST /v1/bind_channel. Payload: {identity_id, channel, external_user_id, display_handle, email, phone}. Subscribe via identity.* family wildcard or specific event. Drives partner CRM mirror pipelines without polling.
ai_usage (alias ai_spend)GETPer-tenant AI cost telemetry (Section 4 substrate). summary sub-object covers today / yesterday / month-to-date call counts + tokens + USD cost + cache hits + errors; by_feature[] breaks down per AI feature (bot / copilot / summary / intent / tone / kb_search) over a trailing-N-days window with pre-computed cache-hit-rate + error-rate + mean-latency. Pre-deploy tenants get a zeroed shape (same JSON, never an error). Backed by dbo.AiUsageEvent + dbo.AiUsageDailyRoll from App_Data/ai_usage_deploy.sql; gateway code that writes into it lands in subsequent blocks.
ai_usage_log (Section 4)data modelAI gateway substrate. Two additive tables: AiUsageEvent (one row per AI call, hot-path indexed on (SiteId, CreatedUtc) + (SiteId, Feature, CreatedUtc)) plus AiUsageDailyRoll (write-time-aggregated rollup keyed on (SiteId, DayBucket, Feature)). Future gateway service writes both via AiUsageRepository.LogCall on every vendor call; dashboards + budget enforcement read from the rollup so admin pages don't scan the event table.
ai_limits (alias ai_guardrails)GETPer-tenant AI cap state. Three independent caps: monthly USD budget, daily token limit, minute call limit (RPM). Null limit = no cap configured. Returns {limit, current, exceeded, headroom, percent_used} per cap plus a top-level any_exceeded + action (soft/hard/none). Future gateway calls AiUsageGuardrails.CheckAll before issuing vendor requests; soft action logs status=budget_exceeded while allowing the call, hard action short-circuits with per-feature fallback (bot → KB-search, copilot → off).
healthGETBlock 284: response now also includes ai_calls_24h + ai_errors_24h + ai_budget_exceeded so external monitors (Pingdom / UptimeRobot / SRE dashboards) see AI tenant health alongside ticket + webhook signals in one endpoint. Errors climbing or budget_exceeded flipping true are alertable; calls is informational. Pre-deploy tenants always get 0/0/false — same JSON shape regardless of state.
setup_statusGET9th probe item channel_foundation reports deploy state of the Section 2 schema — ok when all 5 components present (3 Ticket columns + ChannelMessageVolume + CustomerIdentityChannel), partial mid-migration, missing pre-deploy. Drives setup wizards' "deploy this SQL" hints.

Every response carries the X-MLC-API-Version: v1, X-MLC-Request-Id, and X-RateLimit-* headers. whoami now includes a capabilities block so setup wizards can probe rate limit + auth methods without a separate call. Token introspection is also reachable as resource=me (REST convention from Stripe / GitHub / Slack).

Authentication

Every request needs a token. Issue tokens at /dashboard/config_api_tokens.ascx. The raw secret is shown only once at creation and stored as SHA-256; you cannot recover it later. Pass the token via the Authorization header (preferred):

curl -H "Authorization: Bearer mlc_xxx" \
     https://www.mylivechat.com/v1/api.ashx?resource=ping

Query-string fallback (CORS-friendly, less secure - avoid for production):

curl https://www.mylivechat.com/v1/api.ashx?resource=ping&token=mlc_xxx
CORS: The handler responds with Access-Control-Allow-Origin: * and accepts the verbs GET, HEAD, POST, DELETE, PATCH, OPTIONS. Browser clients can call it directly.

Test your token

Paste your token below to fire a live /v1/whoami call against this server. Token never leaves the browser — the request goes directly from your client to the API. Useful for verifying your token has the right scopes before writing any code.

Postman collection

Block 268: a curated Postman v2.1 collection covering the most-used /v1 shapes — read endpoints, ticket / migration / portal / calendar writes, and a sample subset of CSV exports. Drop it in Postman, set the token collection variable to your bearer token, and start exploring.

Download postman_collection.json

OpenAPI 3.0 spec

Block 269: a curated OpenAPI 3.0 spec covering ~10 most-used endpoints (auth, tickets, customer, CSAT, portal, business calendar, migration, health). Run openapi-generator against it to scaffold a typed SDK client in your language of choice. The full surface is wider than what's in the spec — partners typically use the spec for type-safe scaffolding and the Postman collection for ad-hoc exploration.

Download openapi.json

Scopes

Tokens carry one of two scopes:

ScopeWhat it can do
readGET on every read resource. Cannot mutate state.
read,writeAll read access plus POST / PATCH / DELETE on write resources.

Calling a write method with a read-only token returns 403 forbidden_scope. Pick the smallest scope your integration needs — rotation is cheap.

Rate limit

Each token gets 60 requests per 60-second rolling window. Bursts above the cap return 429 rate_limited with a Retry-After header (seconds). Every response (success and 429) includes:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1714075260   # unix epoch when the window resets

If you need higher throughput, contact support. The bucket is in-memory per AppDomain — an instance recycle resets it; the goal is misbehavior containment, not strict accounting.

Errors & request IDs

Success bodies are {"ok":true,"data":{...}}. Errors are {"ok":false,"error":{"code":"...","message":"...","request_id":"..."}} with the matching HTTP status. Common codes:

HTTPCodeMeaning
400bad_request / missing_field / bad_fieldBody or field invalid.
401unauthorizedMissing or invalid token.
403forbidden_scopeToken lacks write.
404not_found / unknown_resourceResource or row not found for the calling site.
409duplicate_nameUnique-index violation (e.g. tag rename collides).
429rate_limitedBucket exhausted. See Retry-After.
500internal_errorServer-side. request_id is logged - quote it to support.
Always log X-MLC-Request-Id. Server-side 5xx errors are logged under that id; quoting it shortens any support back-and-forth from days to minutes.

Common patterns

Short curl-chain recipes for the most-asked integration scenarios. Copy as a starting point; each can be wrapped in your favorite SDK or scheduler.

Forward an inbound email to a ticket

Backend receives a webhook from your email gateway (SendGrid Inbound Parse, Postmark, etc.), parses out subject/body/from, then:

curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: application/json" \
     -d '{
       "subject":         "$EMAIL_SUBJECT",
       "channel":         "email",
       "priority":        "normal",
       "requester_name":  "$EMAIL_FROM_NAME",
       "requester_email": "$EMAIL_FROM",
       "initial_comment": "$EMAIL_BODY",
       "imported_from":     "sendgrid",
       "imported_external_id": "$MESSAGE_ID"
     }' \
     https://www.mylivechat.com/v1/api.ashx?resource=tickets

The response includes id and initial_comment_id. Round-robin auto-assigns; the ticket.created webhook fires for downstream subscribers.

On-call pager: alert when any ticket breaches SLA

Poll once a minute (well under the 60 req/min budget). Page on any non-empty tickets array.

curl -H "Authorization: Bearer mlc_xxx" \
     "https://www.mylivechat.com/v1/api.ashx?resource=sla_breaches&state=breached&limit=20"

Each row carries worst_minutes_remaining (negative = overdue). Sort is already worst-first, so paging on the first row is enough; PagerDuty / Opsgenie can dedupe on id.

Business-hours-aware paging (suppress 3am false alerts)

The naive on-call pager above pages at any hour of the day. Tenants with a configured business calendar (Block 223) can layer Block 232's health enrichment to suppress alerts that fire when nobody's expected to be at the keyboard. Two cheap calls combine into a single alert decision:

# 1. Are we in business hours right now?
HEALTH=$(curl -s -H "Authorization: Bearer $MLC_TOKEN" \
         "https://www.mylivechat.com/v1/api.ashx?resource=health")
IN_HOURS=$(echo "$HEALTH" | jq -r '.data.is_now_within_business_hours')

# 2. Page only during business hours (or always for the urgent priority)
if [ "$IN_HOURS" = "true" ]; then
  STALE=$(echo "$HEALTH" | jq -r '.data.stale_no_first_reply')
  if [ "$STALE" -gt 0 ]; then
    pagerduty-trigger "Stale tickets during business hours: $STALE"
  fi
fi

# Always page on urgent breaches regardless of calendar
URGENT=$(curl -s -H "Authorization: Bearer $MLC_TOKEN" \
         "https://www.mylivechat.com/v1/api.ashx?resource=sla_breaches&state=breached&priority=urgent&limit=1" \
         | jq -r '.tickets | length')
if [ "$URGENT" -gt 0 ]; then
  pagerduty-trigger "Urgent SLA breach"
fi

This is the standard pattern: business-hours suppress for normal/high stale, always-page for urgent. Adjust the priority threshold to taste. The business_calendar_active field on the same response tells you whether the calendar's actually applied to SLA computation — if it's off, is_now_within_business_hours is informational only and you should fall back to wall-clock alerting (or pick a different rule).

Auto-snooze pending-customer tickets for 4h

When a ticket transitions to pending (subscribe to ticket.status_changed via webhooks or PATCH it yourself), snooze until the next business window:

curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: application/json" \
     -d '{"ticket_id": $ID, "snooze_until_relative": "4h"}' \
     https://www.mylivechat.com/v1/api.ashx?resource=ticket_snooze

Resolve a stale-cohort sweep

Bulk-resolve every open ticket older than N days (e.g. nightly cron). Use the tickets list with a date filter on the client, then:

curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: application/json" \
     -d '{"ticket_ids": [$ID1, $ID2, ...], "action": "resolve"}' \
     https://www.mylivechat.com/v1/api.ashx?resource=tickets_bulk

Up to 200 ids per call; per-row report tells you which actually changed (vs. already-resolved no-ops).

Sync KB articles from your CMS

One-shot create-or-publish from a content-pipeline build step:

# 1. Look up valid categories first (avoids creating articles in unknown buckets)
curl -H "Authorization: Bearer mlc_xxx" \
     https://www.mylivechat.com/v1/api.ashx?resource=kb_categories

# 2. Create + publish in one POST
curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: application/json" \
     -d '{
       "title":    "$DOC_TITLE",
       "slug":     "$DOC_SLUG",
       "category": "$CATEGORY_NAME",
       "body":     "$MARKDOWN_BODY",
       "status":   "visible"
     }' \
     https://www.mylivechat.com/v1/api.ashx?resource=articles

Migrate from Zendesk / Freshdesk / LiveAgent

Two-call shape: dry-run preview then commit. Auto-detect handles header naming differences (Zendesk's requester_email vs Freshdesk's Requester Email) so the same script handles all three sources. The same pattern applies to kb_import for the help-center side.

# 1. Preview — see how the importer would map your CSV
curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: text/csv" \
     --data-binary @export.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=tickets_import&dry_run=1" \
| jq '{platform, total_rows, imported_rows, skipped_rows, sample_preview}'

# 2. Sanity-check sample_preview, then commit
curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: text/csv" \
     --data-binary @export.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=tickets_import&file=export.csv"

# 3. KB articles next — same shape, different resource
curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: text/csv" \
     --data-binary @articles.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=kb_import&file=articles.csv"

# 4. Canned replies (macros) — completes the 3-asset migration
curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: text/csv" \
     --data-binary @macros.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=macros_import&file=macros.csv"

# 5. Zendesk-only: reattach the separate ticket_tags.csv that the
#    Zendesk export emits alongside the ticket file.
curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: text/csv" \
     --data-binary @ticket_tags.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=apply_ticket_tags_bulk"

Re-running step 2 against the same CSV is a no-op — rows are deduplicated on (site_id, platform, external_id). This makes incremental migrations safe: keep exporting + uploading every few hours during cutover and the only rows that actually land are the ones created since the last run.

Daily detractor digest (cron)

Pull the last 24h of CSAT responses with a score of 1 or 2 and email them to the customer-success team. Runs as a cheap nightly cron — uses the score-band filter from Block 209 so the API does the work, no client-side filtering. Pairs with the ticket.csat_low_score webhook for the real-time signal.

# Cron: 0 8 * * *  — every day at 08:00 local
curl -s -H "Authorization: Bearer $MLC_TOKEN" \
     "https://www.mylivechat.com/v1/api.ashx?resource=csat&score_max=2&recent_window_days=1&limit=200" \
| jq '.recent[] | "\(.score)/5 \(.requester_name // .requester_email): \(.comment // "(no comment)")  → ticket #\(.ticket_id)"' \
| mail -s "Detractor digest — last 24h" [email protected]

The same payload drives a Slack-format webhook by piping through jq's slack template, or a CRM enrichment job that pushes the comments back into HubSpot/Salesforce contact notes.

Nightly macro library backup

Export the tenant's shared macro library as CSV every night and rotate it into S3 (or git) for disaster recovery + change tracking. Block 213's CSV export uses the canonical column shape that macros_import consumes, so a backup file restores cleanly into any tenant with one curl.

# Cron: 0 3 * * *  — nightly snapshot
DATE=$(date -u +%Y%m%d)
curl -s -H "Authorization: Bearer $MLC_TOKEN" \
     "https://www.mylivechat.com/v1/api.ashx?resource=macros&format=csv&limit=200" \
     -o /tmp/macros-$DATE.csv

aws s3 cp /tmp/macros-$DATE.csv s3://your-backups/mylivechat-macros/ --quiet

# Restore (one-shot — into a sibling tenant, or after accidental deletion)
curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
     -H "Content-Type: text/csv" \
     --data-binary @macros-20260403.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=macros_import&platform=generic"

The same shape applies to KB articles via articles?format=csv and to tickets via tickets?format=csv — though tickets typically aren't restored from CSV (the audit trail and per-comment history don't round-trip), so the CSV is mainly for compliance / spreadsheet pipelines.

Mirror business calendar from prod to staging

One-shot replication of the SLA business calendar between two tenants — useful when staging needs to mirror production's working hours so SLA dashboards line up across environments. Uses Block 225's GET/PATCH symmetry and Block 223's idempotent upsert.

# 1. Snapshot the source week as JSON
PROD_TOKEN=mlc_prod_xxx
STAGE_TOKEN=mlc_stage_yyy
WEEK=$(curl -s -H "Authorization: Bearer $PROD_TOKEN" \
       "https://www.mylivechat.com/v1/api.ashx?resource=business_calendar" \
       | jq '{week: .week}')

# 2. PATCH it onto the destination
curl -X PATCH -H "Authorization: Bearer $STAGE_TOKEN" \
     -H "Content-Type: application/json" \
     -d "$WEEK" \
     "https://www.mylivechat.com/v1/api.ashx?resource=business_calendar"

# 3. Replicate holidays one at a time (no bulk-upsert by design — keeps
#    accidental holiday drift visible in the audit log)
curl -s -H "Authorization: Bearer $PROD_TOKEN" \
     "https://www.mylivechat.com/v1/api.ashx?resource=business_calendar" \
| jq -c '.upcoming_holidays[]' \
| while read h; do
    DATE=$(echo "$h" | jq -r .date)
    LABEL=$(echo "$h" | jq -r '.label // ""')
    curl -X POST -H "Authorization: Bearer $STAGE_TOKEN" \
         -H "Content-Type: application/json" \
         -d "{\"date_utc\":\"$DATE\",\"label\":\"$LABEL\"}" \
         "https://www.mylivechat.com/v1/api.ashx?resource=business_calendar_holiday" >/dev/null
  done

Add-holiday is idempotent on (site, date) — rerunning the loop the next morning is a no-op for holidays already present, so you can run this on a daily cron without dedupe logic.

Migration onboarding event (CRM enrichment)

Detect when a new tenant first crosses a "real-usage" threshold during a Zendesk/Freshdesk migration — e.g. 100 tickets imported — and push a milestone event into your CRM so customer-success reaches out at the right moment. Uses Block 217's migrate_status aggregate.

# Cron: 0 */4 * * *  — every 4 hours during cutover windows
STATUS=$(curl -s -H "Authorization: Bearer $MLC_TOKEN" \
         "https://www.mylivechat.com/v1/api.ashx?resource=migrate_status")

TICKETS=$(echo "$STATUS" | jq '.tickets.imported_rows')
ARTICLES=$(echo "$STATUS" | jq '.articles.imported_rows')

# Threshold check — both assets at non-trivial counts means real cutover
if [ "$TICKETS" -ge 100 ] && [ "$ARTICLES" -ge 5 ]; then
  curl -X POST https://api.hubapi.com/crm/v3/timeline/events \
       -H "Authorization: Bearer $HUBSPOT_TOKEN" \
       -H "Content-Type: application/json" \
       -d "{
            \"eventTemplateId\":\"$EVENT_TEMPLATE\",
            \"objectId\":\"$CONTACT_ID\",
            \"tokens\":{\"tickets\":$TICKETS,\"articles\":$ARTICLES}
          }"
fi

State-tracking is your CRM's job — this script is stateless. Idempotent at the CRM side (events deduped by content) or write a tiny "high-water mark" file locally if your CRM doesn't.

GDPR subject access request (data export + erasure)

Article 15 of GDPR ("right of access") obliges tenants to deliver a customer's personal data on request, in a portable format, within a reasonable window. The matching erasure request (Article 17) follows the same identity. Two-part workflow:

# 1. ACCESS — pull every artifact that references the requester's email,
#    delivered as flat CSVs ready to zip and email back.
DIR=sar-$(date -u +%Y%m%d)-$(echo "$EMAIL" | tr @ _)
mkdir -p "$DIR"

curl -s -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=visitors&q=$EMAIL&format=csv" \
  -o "$DIR/visitors.csv"

curl -s -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=tickets&q=$EMAIL&format=csv&limit=500" \
  -o "$DIR/tickets.csv"

curl -s -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=audit_log&days=90&format=csv&limit=1000" \
  -o "$DIR/audit_log_90d.csv"

zip -r sar-bundle.zip "$DIR" && rm -rf "$DIR"

# 2. ERASURE — pull the visitor id from step 1, then DELETE.
#    Cascading delete removes events too. Tickets stay (commercial
#    record retention is a separate policy decision).
VID=$(curl -s -H "Authorization: Bearer $MLC_TOKEN" \
       "https://www.mylivechat.com/v1/api.ashx?resource=visitors&q=$EMAIL" \
       | jq -r '.items[0].id')
curl -X DELETE -H "Authorization: Bearer $MLC_TOKEN" \
     "https://www.mylivechat.com/v1/api.ashx?resource=visitors&id=$VID"

Notes:

  • The visitors CSV has the full custom-attribute JSON in attributes_json — downstream consumers should parse and filter to only include data the requester is entitled to under your privacy policy. Keys like internal_score or fraud_risk are likely not in scope.
  • Ticket comment bodies aren't included in the tickets CSV (the dashboard ticket export is per-ticket); for full conversation transcripts use /v1/transcripts per session.
  • Audit log retention defaults to 90 days — longer windows require enterprise tier or self-hosted.
  • Erasure of tickets isn't supported via /v1 (commercial-record retention typically requires keeping the ticket but anonymizing the requester). The dashboard's audit_log captures the visitor delete event.

Snapshot config-as-code (git-tracked tenant config)

Three curl calls capture a tenant's SLA-relevant operational config — flag overrides, business calendar, and SLA policy — as flat CSVs ready to commit to a git repo. Pair with a daily cron and you get a free config diff history with no infrastructure investment.

DATE=$(date -u +%Y%m%d)
DIR=mylivechat-config/$DATE
mkdir -p "$DIR"

curl -s -H "Authorization: Bearer $MLC_TOKEN" \
     "https://www.mylivechat.com/v1/api.ashx?resource=feature_flags&format=csv" \
     -o "$DIR/feature_flags.csv"

curl -s -H "Authorization: Bearer $MLC_TOKEN" \
     "https://www.mylivechat.com/v1/api.ashx?resource=business_calendar&format=csv" \
     -o "$DIR/business_calendar.csv"

curl -s -H "Authorization: Bearer $MLC_TOKEN" \
     "https://www.mylivechat.com/v1/api.ashx?resource=sla_policy&format=csv" \
     -o "$DIR/sla_policy.csv"

cd mylivechat-config && git add . && git commit -m "MyLiveChat config snapshot $DATE" && git push

Restoring is per-resource: PATCH the flags via /v1/feature_flags, PATCH the week via /v1/business_calendar, POST the policy array via /v1/sla_policy. The CSV column shapes are JSON-friendly (string-typed cells map cleanly back to JSON values), so a small jq pipeline reverses each.

Tag library cleanup (merge / disable the long tail)

Tag inventories tend to grow long-tail-heavy on mature tenants — hundreds of tags applied once and forgotten. The applied_count field on /v1/tags is the cleanup signal; combined with the CSV export from Block 216 you get a spreadsheet-driven workflow without writing a single line of glue code.

# 1. Pull the inventory as CSV, sort by applied_count ascending
curl -s -H "Authorization: Bearer mlc_xxx" \
     "https://www.mylivechat.com/v1/api.ashx?resource=tags&format=csv" \
| sort -t, -k6 -n > tags-by-usage.csv

# 2. Pick the long tail in Excel — every tag with applied_count == 0 is a
#    candidate for disable (PATCH enabled=false) or hard delete (DELETE).

# 3. Disable a single tag — soft, reversible
curl -X PATCH -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: application/json" \
     -d '{"id": $TAG_ID, "enabled": false}' \
     https://www.mylivechat.com/v1/api.ashx?resource=tags

# 4. Hard delete (only when applied_count is 0 — server enforces)
curl -X DELETE -H "Authorization: Bearer mlc_xxx" \
     "https://www.mylivechat.com/v1/api.ashx?resource=tags&id=$TAG_ID"

The same pattern applies to KB categories via kb_categories?format=csv + the dashboard's category management page (categories with article_count==0 are good merge candidates).

Set up the customer-facing portal

End-to-end recipe for the magic-link portal (Blocks 244–248, 263). Once configured, requesters can view + reply to their own tickets without an account, agents can send portal links from the ticket-detail page, and new tickets auto-mint links on creation if you want full automation.

  1. Run the deploy SQL: App_Data/portal_token_deploy.sql creates dbo.PortalToken. Strictly additive; safe to re-run.
  2. Subscribe to the issuance webhook: create a subscription for ticket.portal_token_issued pointing at your email-automation tool (Customer.io, Mailgun routes, Zapier, n8n). The payload includes the raw token; your downstream sends a "Click here to view your tickets" email containing https://www.mylivechat.com/portal/?token=<token>. Treat the webhook payload as a secret — same HMAC verification you do for any other webhook (timing-safe snippets here).
  3. Pick your delivery path:
    • Agent-driven (default): keep portal.auto_issue_on_ticket_create off. Agents click "Send portal link" on the ticket-detail page when a requester needs one (Block 247). Token + webhook fire on click.
    • Auto-mint on every ticket: turn on portal.auto_issue_on_ticket_create via /dashboard/config_feature_flags.ascx or PATCH /v1/feature_flags. Every ticket landing with a requester email mints a token. Idempotent — existing active tokens are reused so the same requester doesn't get five different links.
  4. (Optional) embed self-service login: link to /portal/login.aspx?site=<your_site_id> on your support page. Requesters who lost their email enter their address; same token-issue path fires; same webhook delivers. Page is anti-enumeration (silent on whether the email matched).
  5. (Optional) bulk-mint for migrated requesters: after Block 203's tickets_import populates a fresh tenant from Zendesk, loop through the imported emails and call POST /v1/portal_token per email so every migrated requester gets a portal link delivered:
    curl -H "Authorization: Bearer $MLC_TOKEN" \
      "https://www.mylivechat.com/v1/api.ashx?resource=customer_identities&limit=200&format=csv" \
    | awk -F, 'NR>1 && $2!="" { print $2 }' \
    | while read EMAIL; do
        curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
             -H "Content-Type: application/json" \
             -d "{\"requester_email\":\"$EMAIL\",\"reuse_active\":true}" \
             "https://www.mylivechat.com/v1/api.ashx?resource=portal_token" >/dev/null
      done

Verify end-to-end via migrate_status — the portal_tokens.active_tokens + portal_tokens.total_uses counters tell you whether the loop is working without manually checking individual tickets.

Channel webhook receiver pattern (Section 5 build template)

Reference pattern for the per-channel inbound webhook handlers (WhatsApp via Twilio, Messenger via Meta, SMS via Twilio, Instagram DM, SendGrid Inbound Parse). All share the same skeleton — only payload-parsing + per-channel quirks (Twilio media URL exfiltration, Meta page-token, etc.) differ. Use this as the starting point when the real handler lands; the prerequisites it depends on (signature verifier, identity repository, volume meter) all exist today.

// Pseudo-code — exact namespacing depends on which assembly
// ends up holding the channel handler (HostChatSubsite hot-path
// vs. HostChatWebsite alongside the API). Both placements work;
// pick when you wire the real receiver.

public void ProcessRequest(HttpContext ctx) {
    if (ctx.Request.HttpMethod != "POST") { Status405(); return; }

    // 1. Read raw body BEFORE form parsing — must hash exactly
    //    what the sender hashed, byte-for-byte.
    byte[] body = ReadAllBytes(ctx.Request.InputStream);

    // 2. Resolve tenant from URL path / query (real handlers
    //    embed siteId in the webhook URL Twilio/Meta posts to).
    int siteId = ResolveTenant(ctx.Request);

    // 3. Per-tenant secret lookup. NEVER hardcode; NEVER trust
    //    the request to tell you what its own secret is.
    string secret = SiteIntegrationSettingsRepository.Get(
        siteId, "whatsapp.twilio_auth_token");

    // 4. Signature verification (Block 273 helper).
    //    Twilio: VerifyTwilio(secret, fullUrl, form, sigHeader)
    //    Meta:   VerifyHmacSha256Hex(secret, body, x-hub-signature-256)
    if (!WebhookSignatureVerifier.VerifyTwilio(secret,
            ctx.Request.Url.AbsoluteUri,
            ParseFormParams(body),
            ctx.Request.Headers["X-Twilio-Signature"])) {
        Status403("bad_signature"); return;          // log + drop
    }

    // 5. Parse vendor-specific payload to (externalUserId,
    //    threadId, text, mediaUrls, displayHandle).
    var msg = ParseTwilioInbound(body);

    // 6. Identity resolution. Hot path: known sender (~1ms via
    //    filtered unique on (SiteId, Channel, ExternalUserId)).
    var identity = CustomerIdentityRepository.GetByExternalUser(
        siteId, "whatsapp", msg.ExternalUserId);

    long identityId = identity?.Id ?? 0;
    if (identityId == 0) {
        identityId = CustomerIdentityRepository.GetOrCreate(
            siteId, email: "", phone: msg.ExternalUserId,
            displayName: msg.DisplayHandle);
        CustomerIdentityRepository.BindExternalHandle(
            siteId, identityId, "whatsapp",
            msg.ExternalUserId, msg.DisplayHandle);
    }

    // 7. Media exfiltration (WA-3) — Twilio media URLs expire.
    //    Real handler downloads + persists to tenant blob storage
    //    BEFORE returning 200, replacing the URLs in msg.MediaUrls.
    foreach (var u in msg.MediaUrls) {
        msg.MediaUrls[u.Index] = BlobStorage.Persist(siteId, u);
    }

    // 8. Enqueue for downstream processing (Section 3 durable
    //    queue). Real handler does NOT process inline — Twilio
    //    retries any 5xx aggressively, so under heavy load you
    //    want the receive path under 500ms.
    InboundQueue.Enqueue(siteId, "whatsapp", identityId, msg);

    // 9. Volume metering (Block 275). Best-effort — not billing.
    ChannelMessageVolumeRepository.IncrementInbound(siteId, "whatsapp");

    Status200(); // keep response tiny; vendor doesn't read it
}

Downstream, the queue worker:

  • Resolves or creates the conversation/ticket row using (siteId, channel, externalThreadId) from dbo.Ticket.IX_Ticket_ChannelThread.
  • Updates Ticket.LastInboundUtc for WhatsApp's 24-hour service-window logic (WA-4).
  • Persists the message body + media references onto the conversation transcript.
  • Fires ticket.requester_replied (existing ticket) or ticket.created (new conversation) so subscribers see it.
  • Notifies any agent listening on the conversation via the existing real-time push channel.

This shared pattern is what makes Sections 5A-5E feel like one feature instead of five. Per-channel handlers swap the steps 3-7 specifics; everything else is identical.

Pre-bind channel handles before launch (Section 5 onboarding)

End-to-end recipe for the Section 2 channel-foundation surface (Block 273/274). When you're spinning up a tenant on WhatsApp / Messenger / SMS, the goal is for the very first inbound webhook to thread into a known customer identity rather than minting an orphan that has to be merged later. The flow is: deploy the schema → mirror your CRM's known channel handles into MLC via POST /v1/bind_channel → subscribe to identity.channel_bound for ongoing sync.

  1. Apply the deploy SQL: App_Data/channel_foundation_deploy.sql adds ExternalThreadId / ExternalUserId / LastInboundUtc to dbo.Ticket, plus dbo.ChannelMessageVolume rollup and dbo.CustomerIdentityChannel junction. Strictly additive; safe to re-run. Verify via /v1/setup_status — the channel_foundation probe should report "status":"ok".
  2. Pre-bind known handles from your CRM: walk your existing customer rows and POST the channel handles you already have on file. Idempotent — re-running the same script doesn't create duplicates, just refreshes last_seen_utc:
    # Example: bulk-bind WhatsApp handles from a CSV of (email, wa_e164)
    while IFS=, read -r EMAIL WA_PHONE; do
      curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
           -H "Content-Type: application/json" \
           -d "{\"email\":\"$EMAIL\",\"channel\":\"whatsapp\",
                \"external_user_id\":\"$WA_PHONE\"}" \
           "https://www.mylivechat.com/v1/api.ashx?resource=bind_channel"
    done < known_whatsapp_handles.csv
  3. Subscribe to identity.channel_bound: create a webhook subscription for the identity.* family pointing at your CRM mirror endpoint. Future binds (whether via API, agent stitching, or downstream channel webhook) will mirror automatically. Verify the standard HMAC X-MLC-Signature header — same code path you use for any other event.
  4. Verify in the agent UI: open /dashboard/[email protected] for one of the bound customers. The "Reachable on:" pill row should appear below the identity badge with one pill per channel handle (icon + label + truncated handle). Hover for first/last seen timestamps.
  5. Verify the read API: GET /v1/[email protected] returns the identity.channels[] array. Empty array = no handles bound yet; non-empty = ready for the channel build.

Once Section 5A WhatsApp ships, this same pre-bind step makes the difference between "first inbound message creates conversation #4231 against the existing customer" and "first inbound message creates orphan #4231, agent has to manually merge to identity #88 later". Tenants migrating from a platform that already stored channel handles can mass-bind in one batch and skip the merge dance entirely.

Webhook deliveries retention (nightly purge)

The dbo.WebhookDelivery table accumulates one row per outbound webhook attempt. On a chatty tenant subscribed to ticket.* + article.* wildcards, that's 100s of thousands of rows in a month — leaves dashboard read paths slowing measurably (per runbook #10). Block 254 ships a targeted purge endpoint that nightly cron can call.

# Cron: 0 3 * * *  — keep 30 days of delivered rows; failed/pending
#                   are NEVER touched (those are the ones humans triage)
curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=webhook_deliveries_purge&days=30"

# Response: { "purged": 4218, "older_than_days": 30, "capped_at_100k": false }

Constraints: days clamp 7..365 (refuses fresher than a week so a typo can't blow away troubleshooting data); per-call cap 100,000 rows so a single POST can't run for hours on a months-behind tenant. Tenants whose backlog is bigger re-run nightly and catch up incrementally — the cap is intentional, not a budget. Pairs with the webhook_deliveries + webhook_subscriptions CSV exports for compliance archival before purging if your retention policy requires it.

Health monitoring (UptimeRobot / Pingdom)

Use the unauthenticated heartbeat for the public probe (no token cost, no rate limit), then a cheap authenticated /v1/health from inside your VPC to alert on tenant-scoped queue depth:

# Public liveness — point UptimeRobot here
curl https://www.mylivechat.com/v1/api.ashx?resource=heartbeat

# Authenticated tenant snapshot — alert on stale_no_first_reply > 0
# OR webhook_failed_24h > 0
curl -H "Authorization: Bearer mlc_xxx" \
     https://www.mylivechat.com/v1/api.ashx?resource=health

Runbooks

Operational procedures for the most-common production incidents. Each entry follows the same structure: signal (what tells you something's wrong) → diagnostic (one-line confirmation it's the thing you think it is) → fix (what to actually do). Designed to be readable mid-incident — keep the page open in the on-call tab.

1. Webhook deliveries failing

Signal: the topbar pill on the dashboard reads "N webhook fail(s)" or /v1/health returns webhook_failed_24h > 0.

Diagnostic:

curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=webhook_deliveries&status=failed&limit=10" \
  | jq '.deliveries[] | {subscription_label, event_type, last_http_code, last_error}'

Fix: by failure mode:

  • last_http_code: 401/403 — subscriber's signature secret is stale. Have the integration owner re-pull the subscription's secret on their side, or rotate via POST /v1/rotate_webhook_secret and re-share. (Caveat: rotation invalidates in-flight retries — accept the data loss or pause new events first.)
  • last_http_code: 404/410 — subscriber endpoint moved/decommissioned. Update the URL via PATCH /v1/webhook_subscriptions, or DELETE the subscription if the integration is dead.
  • last_http_code: 500/502/503 — subscriber outage. Wait for retries to drain (the dispatcher backs off exponentially). If the queue grows, page the subscriber's owner.
  • last_http_code: 0 + last_error: timeout / connect refused — receiver is unreachable. Same fix as the 5xx path; usually transient.

2. Webhook queue depth growing

Signal: /v1/health returns webhook_queue_pending climbing over time, or the dashboard "deliveries" panel shows pending rows accumulating.

Diagnostic: Look at by_status across the full window:

curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=webhook_deliveries&days=1" \
  | jq '.by_status'

If pending is >100 and the same subscription_id dominates the failures, you've found the slow subscriber.

Fix: isolate the offending subscription, then either disable it (PATCH /v1/webhook_subscriptions {enabled:false}) to drain the queue and protect throughput for the others, or contact the subscriber's owner. Re-enable when their endpoint is back.

3. SLA breach pager firing at 3am

Signal: on-call gets paged outside business hours about a "stale ticket".

Diagnostic: Did the calendar feature get configured?

curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=health" \
  | jq '.data | {is_now_within_business_hours, business_calendar_active}'

Fix: if business_calendar_active=false, the SLA engine is using wall-clock minutes regardless of the calendar — turn on the sla.use_business_calendar feature flag at /dashboard/config_feature_flags.ascx. If the flag is on but the calendar isn't configured for this tenant, walk through /dashboard/config_business_calendar.ascx and apply a preset (US federal / UK bank / EU common) plus the right weekly hours. The pager script should use Block 233's recipe to suppress alerts off-hours.

4. Stale-tickets pill won't go away

Signal: dashboard topbar shows "N stale" but admins claim they replied.

Diagnostic:

curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=sla_breaches&state=breached&limit=5" \
  | jq '.tickets[] | {ticket_number, priority, first_response_utc, response_minutes_remaining}'

The trick: a "reply" only resets the SLA if it's a public reply (not an internal note) AND the ticket's FirstResponseUtc column was empty before. If admins are using internal notes for their replies, the SLA never sees them.

Fix: have admins use the public-reply composer rather than internal notes. For tickets already breached, post a public reply to backfill FirstResponseUtc; the pill will clear within ~60s of the next dashboard load.

5. Migration importer reports zero imported rows

Signal: tickets_import POST returns imported_rows=0, skipped_rows=N and the tenant insists they have new data.

Diagnostic: Run with ?dry_run=1 first and inspect sample_preview:

curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
  -H "Content-Type: text/csv" --data-binary @export.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=tickets_import&dry_run=1" \
  | jq '.sample_preview[] | {subject, would_skip_duplicate, external_id}'

Fix:

  • If would_skip_duplicate=true on every preview row: every row's external_id already exists in this tenant. The dedup is working as designed; tenant is re-uploading the same export. Verify they actually exported new rows from their source system.
  • If platform=null in the response: auto-detect failed. Pin the platform with ?platform=zendesk (or freshdesk / liveagent).
  • If most rows have empty external_id: source platform's CSV doesn't expose its native ID column, so dedup runs on title only — which means only the first import round-trips work. Ask the source to include an ID column in the export.

6. CSAT scores aren't landing

Signal: CSAT settings page shows Surveys issued > 0 but Responses received = 0 after several weeks.

Diagnostic: Check whether the ticket.csat_issued webhook is firing:

curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=webhook_deliveries&limit=20" \
  | jq '.deliveries[] | select(.event_type=="ticket.csat_issued") | {status, last_http_code}'

Fix: if no csat_issued events appear, the tenant has no webhook subscription for that event — delivery was never attempted, so the survey email never went out. Either subscribe the tenant's email-automation tool (Zapier / Customer.io) at /dashboard/config_webhooks.ascx, or temporarily flip csat.auto_issue_on_resolve off if they don't want surveys at all. If events ARE firing but failing (last_http_code != 200), see runbook #1.

7. Dashboard renders raw {key.dotted} placeholders

Signal: a dashboard page shows literal text like {flags.title} or ??ui_missing??.

Diagnostic: The DashboardLang i18n helper falls back to English silently for missing keys, so raw placeholder text means a hard failure (not a missing translation). Most likely: App_Data/dashboard_lang/en.json is corrupted or missing.

Fix: verify the file parses:

python -c "import json; json.load(open('App_Data/dashboard_lang/en.json'))"

If it errors, restore from git. The helper caches per culture for 60s; once the file mtime advances, the cache rebuilds on next request — no IIS bounce needed.

8. AI bot answering with hallucinations

Signal: users in chat reporting bot answered with confidently-wrong info.

Diagnostic: Check the AI chat log: /dashboard/config_ai_chatlog.ascx for the recent conversation. Look at which KB articles the bot retrieved.

Fix: the bot retrieves from the tenant's KB articles + an internal knowledge cache. Hallucinations almost always trace to either (a) outdated/wrong KB content the bot's reading as authoritative, or (b) the user's question being outside the KB and the bot guessing. For (a), edit the offending article; for (b), use no-result-search analytics to see what users are asking that the KB doesn't cover, and seed missing articles. Consider flipping ai.copilot off temporarily if hallucinations are recurring while you fix the KB.

9. Tenant flag changes don't take effect

Signal: admin flips a feature flag on config_feature_flags.ascx, but the affected feature still behaves as before.

Diagnostic: FeatureFlagRepository caches per-(siteId, flagKey) in-process for 60 seconds. If you JUST flipped the flag, wait a minute and retry. If the cache TTL has expired and the behavior persists, the consuming code path may not be reading the flag — check the source code path that's expected to honor it.

Fix: if the cache is stale, an IIS app-pool recycle clears it instantly, but the 60s wait is usually faster than the recycle. If the consuming code doesn't honor the flag at all, that's a code defect — file a bug citing the flag key + the expected behavior.

10. Database performance degrading on tickets list

Signal: /dashboard/tickets.ascx page load > 2s; analytics.ascx per-card sparklines slow.

Diagnostic: SQL Server Activity Monitor for the SiteId in question; look for table scans on dbo.Ticket or dbo.AnalyticsEvent.

Fix: usual suspects:

  • Tenant has >1M tickets and the dashboard is doing full-text scans on Subject. Mitigation: full-text catalog on Subject, or move to ES.
  • Analytics-events table is unbounded. Schedule a daily job to roll up old events into dbo.AnalyticsDailySummary and prune originals older than 90 days.
  • A single tenant's WebhookDelivery table grew to millions of rows. The retention sweep at /dashboard/config_webhooks.ascx manual-prune button helps; long-term, schedule a nightly delete of Status=delivered AND CompletedUtc < DATEADD(DAY,-30,...).

11. Tenant data isolation audit (SOC 2 / compliance)

Signal: auditor asks "prove customer A can't see customer B's data" or you're investigating a suspected leak.

Diagnostic: the project's invariant is that every tenant-scoped table has a SiteId column and every query in the C# layer adds a WHERE SiteId = @Sid clause. Verify the schema invariant holds:

-- Every tenant-scoped table MUST have a SiteId column.
-- Run this in SSMS; the result should be empty (every
-- table that touches tenant data is enumerated below).
SELECT t.name AS TableName
FROM sys.tables t
WHERE t.name IN (
    'Ticket','TicketComment','TicketActivity','TicketPortalToken',
    'TicketImportRun','TicketMacro','TicketSavedView','TicketSnooze',
    'TicketTimeLog','TicketSkill','SlaPolicy','BusinessCalendar',
    'KbArticle','KbArticleVersion','KbCategory','KbImportRun','KbFeedback',
    'WebhookSubscription','WebhookDelivery','SiteIntegrationSettings',
    'CustomerIdentity','CustomerIdentityChannel','PortalToken',
    'ChannelMessageVolume','AiSiteBilling','AiUsageEvent','AiUsageDailyRoll',
    'FeatureFlag','AssignmentRotation','AgentSkill','MacroImportRun',
    'AnalyticsEvent','AnalyticsDailySummary'
  )
  AND NOT EXISTS (
    SELECT 1 FROM sys.columns c
    WHERE c.object_id = t.object_id AND c.name = 'SiteId'
  );

Any row returned is a schema-isolation bug — file a P1.

Probe the C# query layer: every repository method that takes a siteId parameter must include it in the WHERE clause. Spot-check via grep:

# Every "FROM dbo.Ticket" should be paired with a "WHERE SiteId="
# (or part of a JOIN that filters tenancy upstream)
grep -rn "FROM dbo.Ticket" HostChatWebsite/App_Code/ \
  | grep -v 'WHERE.*SiteId\|JOIN'

Findings without an obvious tenant filter need human review — sometimes a JOIN tenancy-filters upstream, sometimes not.

Probe the API layer: every /v1/api.ashx handler resolves siteId from the bearer token and passes it to repository methods. Verify no handler accepts a tenant-id parameter from query/body that bypasses token-derived auth:

grep -rn "ctx.Request.QueryString\[\"site_id\"\]" HostChatWebsite/v1/  # expect zero hits
grep -rn "JsonGetInt(body, \"site_id\")"        HostChatWebsite/v1/  # expect zero hits

Fix: any leak surface found gets a P1 with a regression test pinning the new behavior. Document the audit run in the SOC 2 evidence binder — auditors want the date you ran it, the command output, and the disposition of any findings.

The audit is a periodic ops task. Recommend running monthly (and after every release that touches a repository or v1 handler) until SOC 2 Type II observation period closes.

12. Email-to-ticket inbound failures

Signal: a tenant reports "I'm not getting tickets from forwarded emails anymore," or the EmailInboundAudit table shows a spike of non-success outcomes.

Diagnostic: the /livechat3/email-inbound.ashx receiver writes one audit row per inbound email regardless of outcome. Group by outcome over the trailing 24h:

SELECT
    Outcome,
    COUNT(*) AS Count,
    MIN(ReceivedUtc) AS FirstSeen,
    MAX(ReceivedUtc) AS LastSeen
FROM dbo.EmailInboundAudit
WHERE ReceivedUtc >= DATEADD(HOUR, -24, SYSUTCDATETIME())
GROUP BY Outcome
ORDER BY Count DESC;

Outcome values map to specific failure modes:

  • ticket_created — happy path. New ticket minted from the inbound email.
  • reply_appended — happy path. Reply threaded into an existing ticket via the ticket-{id}@reply.… address pattern.
  • unrecognized_recipient — the To: address didn't match any tenant's configured inbox. Common causes: tenant changed their forwarding rule, DNS MX record points to the wrong place, or the address pattern in the integration setting drifted from what the forwarder uses. Fix: have the tenant verify /dashboard/email_to_ticket.ascx shows the address they're forwarding to.
  • missing_recipient — webhook payload arrived without a To: header. Almost always a misconfigured forwarder; ask the tenant to switch from BCC-style forwarding to To-address forwarding.
  • error:<type> — exception during processing. Drill into FromAddr + Subject + ReceivedUtc for that row, cross-reference with application logs from the same minute, file as P2 unless errors are spiking (then P1).

Fix: by mode:

  • Single tenant pattern — read recent rows for that tenant's recipient address, walk through forwarding setup with them. The /dashboard/email_to_ticket.ascx page documents the expected MX / forwarder behavior.
  • Cross-tenant spike of unrecognized_recipient — your inbound DNS / MX record changed or the receiver's tenant-resolution logic regressed. File P1.
  • Cross-tenant spike of error:* — receiver bug. File P1; rollback the most recent deploy if the spike correlates with release time.

Audit table is append-only and retention-friendly (no PII beyond email addresses). Ops periodic prune of rows older than 90 days is fine — happy-path rows are bulk and the table grows linearly with traffic.

13. AI runaway tenant (cost spike alert)

Signal: a single tenant's AI cost spikes 10x or token consumption jumps to a multiple of their typical baseline. Often surfaces from finance/billing rather than infra dashboards. Section 4 of the strategic plan: "a single misconfigured tenant must not destroy unit economics."

Diagnostic: rank tenants by current-month AI spend to identify the outlier. Run as ops (not as a tenant-scoped token):

-- Top-20 tenants by current-month AI spend
DECLARE @month DATE = DATEFROMPARTS(YEAR(SYSUTCDATETIME()), MONTH(SYSUTCDATETIME()), 1);
SELECT TOP 20
    SiteId,
    SUM(CallCount)   AS Calls,
    SUM(TokensIn + TokensOut) AS Tokens,
    SUM(CostUsd)     AS CostUsd,
    SUM(ErrorCount)  AS Errors,
    MAX(UpdatedUtc)  AS LastUpdate
FROM dbo.AiUsageDailyRoll
WHERE DayBucket >= @month
GROUP BY SiteId
ORDER BY CostUsd DESC;

Then drill into the per-feature breakdown for the top tenant via the per-tenant API:

curl -H "Authorization: Bearer $TENANT_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=ai_usage&days=7&include=trend"

Three failure modes to distinguish:

  • Misconfigured prompt loop — one feature's calls count is 10x its baseline. Often a "the bot keeps calling the bot" recursion in customer integration code. error_rate is usually low because the calls succeed; the cost just compounds.
  • Abusive end-user — by_feature.bot calls spike from one IP / session, often with prompt-injection payloads. Errors may rise as the vendor's safety filters trigger.
  • Genuine viral growth — trend shows a smooth ramp, not a spike. Errors and cache-hit rate are normal. Tenant just got their hockey-stick moment.

Fix: mitigation depends on mode:

  • For the prompt loop / abuse cases: set a hard daily token cap immediately. Use PATCH /v1/ai_limits with the tenant's token (or impersonate via ops):
    curl -X PATCH -H "Authorization: Bearer $TENANT_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"daily_token_limit": 100000, "action": "hard"}' \
      "https://www.mylivechat.com/v1/api.ashx?resource=ai_limits"
    The cap takes effect on the next gateway call (cache invalidates on PATCH per Block 289). Tenant gets a friendly fallback (KB-search-only for bot, copilot disabled) instead of unbounded vendor spend.
  • For viral growth: bump the cap rather than enforce. Customer success reaches out to discuss plan upgrade if the tenant is on the free or starter tier.
  • For ALL modes: file a finance reconciliation note so the tenant's invoice matches the actual incurred cost. The cost_usd column is best-effort estimate; vendor invoices are authoritative.

Long-term: when SOC 2 lands, the runaway-tenant signal should auto-page (not wait for finance to notice). The strategic plan's "cost dashboard for internal team" is the missing piece — a single rolled-up view across all tenants would surface this within an hour rather than at end-of-month.

14. AI vendor outage / errors spiking

Signal: /v1/health returns ai_errors_24h climbing (Block 284), or admins report bot answers timing out / replying with vendor error text.

Diagnostic: per-feature breakdown to find which feature path is failing:

curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=ai_usage&days=1" \
| jq '.by_feature[] | select(.errors > 0) | {feature, errors, error_rate, mean_latency_ms}'

If error_rate > 0.05 for any feature, treat as a vendor incident.

Fix: by failure mode:

  • Vendor (Anthropic / OpenAI) is down or throttling — check the vendor's public status page. Per the strategic plan's Section 4 fallback policy, a 4hr Anthropic outage should: copilot disabled, bot falls back to KB-search-only. Until the gateway service lands with that fallback wired, manual workaround is to flip the ai.copilot + ai.bot feature flags off via /dashboard/config_feature_flags.ascx, then back on once the vendor recovers.
  • Single tenant runaway spend — /v1/health shows ai_budget_exceeded=true. Read /v1/ai_limits to confirm which cap. If the cap is set but the action is soft, errors are informational; if hard, the gateway is short-circuiting and per-feature fallback is active. Either bump the cap (UPDATE dbo.AiSiteBilling) or wait for the next month-bucket rollover for budget caps.
  • Per-tenant API key revoked — by_feature errors will be 100% for that tenant. Check SiteIntegrationSettings for stale provider keys; have the customer rotate.
  • Network / DNS — mean_latency_ms spikes alongside errors. Confirm the host can reach the vendor endpoint. Often resolves itself within minutes; if not, fail over to the fallback model family per Section 4 plan (Sonnet → Haiku for non-customer-facing).

Long-term: when the AI gateway service lands, this runbook collapses to "check the gateway's vendor-outage circuit-breaker dashboard" since the gateway will own the fallback logic uniformly across features.

GET heartbeat (no auth)

/v1/api.ashx?resource=heartbeat

Unauthenticated liveness probe. Designed for external uptime monitors (UptimeRobot, Pingdom, status.io) that want to poll a public URL without burning a real token's rate budget. Bypasses both auth and rate-limiting on purpose — the body intentionally exposes nothing tenant-specific.

{
  "ok": true,
  "data": {
    "ok": true,
    "service": "mylivechat-public-api",
    "api_version": "v1",
    "server_utc": "2026-04-29T18:00:00.000Z"
  }
}
Live status: probing…

Use /v1/whoami instead if you need to verify a specific token, or /v1/health if you want tenant-scoped queue depth (both require auth).

GET whoami / me

/v1/api.ashx?resource=whoami
/v1/api.ashx?resource=me

Token introspection. Returns the calling token's site_id, scopes, and lifetime usage. Useful for setup wizards / Terraform providers that need to validate a supplied token before applying changes. The me alias matches the REST convention every major API uses for this (Stripe, GitHub, Slack, Twilio).

{
  "ok": true,
  "data": {
    "token_id": 17,
    "token_prefix": "mlc_zapier_",
    "site_id": 10000324,
    "label": "zapier-prod",
    "scopes": ["read","write"],
    "request_count": 4823,
    "created_utc": "2026-04-12T14:21:08.847Z",
    "last_used_utc": "2026-04-25T09:14:31.001Z",
    "capabilities": {
      "api_version": "v1",
      "rate_limit_per_min": 60,
      "supports_patch": true,
      "supports_bulk": true,
      "auth_methods": ["bearer", "query_token"],
      "csv_exporters": [
        "tickets","articles","audit_log","macros","tags",
        "saved_views","kb_categories","webhook_subscriptions",
        "webhook_deliveries","feature_flags","business_calendar",
        "agents","sla_policy","visitors","csat","feedback",
        "customer_identities","portal_tokens","mentions",
        "time_log","article_versions","kb_feedback"
      ],
      "deployed": {
        "portal_token":       true,
        "customer_identity":  true,
        "business_calendar":  true,
        "ticket_import_run":  true,
        "kb_import_run":      true,
        "macro_import_run":   true,
        "agent_skill":        true,
        "feature_flag":       true,
        "channel_foundation": true,
        "ai_usage_log":       true
      }
    }
  }
}

Block 237: capabilities.csv_exporters lists every resource that supports ?format=csv. SDK setup wizards use this to render "your token can export X / can't export Y" UX without hardcoding the table on their side — new exporters appear automatically as the API grows.

Block 267: capabilities.deployed reports which optional schema additions are present on this tenant. SDK wizards use it to adapt the UI — e.g. only show "Portal" tab when portal_token: true, only show "Migrate" widget when the three import-run tables are present. Pre-deploy or partially-deployed tenants get false against the missing keys; an empty deployed: {} object is also possible if the OBJECT_ID probe itself failed.

Block 274: channel_foundation rolls up the five Section 2 schema components into one boolean — additive Ticket columns + ChannelMessageVolume + CustomerIdentityChannel must all be present. SDK wizards gate channel-related UI ("Connect WhatsApp", channel-handle pre-bind) on this single flag rather than probing five OBJECT_IDs themselves.

GET resources

/v1/api.ashx?resource=resources

Self-describing catalog of every /v1 resource and its supported HTTP methods. Designed for SDK generators and auto-config setup wizards that don't want to scrape this docs page. The shape is stable across releases so you can diff resources output between deployments to see what's new. Alias: resource=capabilities.

{
  "ok": true,
  "data": {
    "api_version": "v1",
    "rate_limit_per_min": 60,
    "count": 41,
    "resources": [
      {"name": "ping",     "methods": ["GET"],                       "summary": "Liveness + version check."},
      {"name": "ticket",   "methods": ["GET","POST","PATCH"],        "summary": "Single ticket (read | create | mutate ...)."},
      {"name": "macros",   "methods": ["GET","POST","PATCH","DELETE"],"summary": "Shared canned-reply macros."}
      ...
    ]
  }
}

GET tags

/v1/api.ashx?resource=tags[&format=csv]

List every conversation tag for the calling site. Each row carries {id, name, color, description, enabled, created_utc, applied_count}. applied_count is the live count of confirmed applications across ConversationTagApplied — useful for "merge unused tags" cleanup tools and tag-inventory dashboards without a follow-up join.

Block 216: ?format=csv emits the same row set as text/csv with a timestamped filename and RFC 4180 escaping. Drives the spreadsheet-driven cleanup workflow — tenants paste into Excel, sort by applied_count, identify the long tail of single-use tags to merge or disable.

# Find every tag with zero applications — candidates for cleanup
curl -s -H "Authorization: Bearer mlc_xxx" \
     "https://www.mylivechat.com/v1/api.ashx?resource=tags&format=csv" \
| awk -F, 'NR>1 && $6==0 { print $2 }'

GET conversation_starters

/v1/api.ashx?resource=conversation_starters

The visitor-facing prompt chips that render above the chat input. Useful for drift-detection tooling or to mirror current starters into a CMS.

GET handoffs

/v1/api.ashx?resource=handoffs[&limit=N]

Recent AI → human handoff records. limit defaults to 50 (max 200).

GET transcripts

/v1/api.ashx?resource=transcripts[&limit=N]

Recent chat transcripts (visitor + agent + AI messages). Same shape used by the dashboard's Chat Log.

GET tickets / ticket

/v1/api.ashx?resource=tickets[&status=...][&assignee=<agentId>|unassigned|snoozed][&priority=urgent|high|normal|low][&q=...][&limit=N][&skip=N][&format=csv]
/v1/api.ashx?resource=ticket&id=N[&include=comments,activity,links,sla,customer]

Help-desk tickets. The list endpoint mirrors the agent dashboard's Tickets table, including per-status counts. The single-ticket endpoint accepts include=comments,activity,links,sla,customer to expand the conversation thread, full activity history, cross-ticket references, SLA breach state, or a lightweight customer rollup; internal notes are filtered out on every API response — they're agent-only.

The list response also includes an applied_filters echo of the active {status, assignee, priority, q, skip, limit} — useful for clients building "next page" links without tracking filter state on their side.

Pass format=csv to receive the same row set (post-filter, post-paging) as text/csv with a timestamped Content-Disposition filename. Same RFC 4180 escaping as /v1/audit_log's CSV export. 15-column shape: id, ticket_number, subject, status, priority, channel, assignee_agent_id, requester_name, requester_email, requester_phone, tags, first_response_utc, resolved_utc, created_utc, updated_utc.

include=sla returns: {priority, response_target_minutes, resolution_target_minutes, response_state ("ok"|"warning"|"breached"), response_minutes_remaining (negative when breached), resolution_state, resolution_minutes_remaining}. Targets reflect the per-tenant sla_policy overrides, falling back to defaults when no row exists.

include=customer returns: {email, phone, other_tickets_total, other_tickets_open, first_seen_utc, last_seen_utc, csat_count, csat_avg, is_repeat}. Lightweight by design — for the full ticket / CSAT history call customer directly with the same email or phone.

Tickets are read-only via API. Create paths: chat→ticket conversion (/dashboard/convert_to_ticket.aspx?sessionid=N), manual create form, or CSV import (Zendesk / Freshdesk / LiveAgent — idempotent on imported_external_id).

Subscribe to ticket events via the webhook_subscriptions POST with events=ticket.created,ticket.replied,ticket.resolved,ticket.mentioned (or events=ticket.* for all). Receivers verify HMAC-SHA256 in the X-MLC-Signature header.

ticket.mentioned fires when an internal note tags one or more agents via @loginname. Payload includes {ticket_id, comment_id, actor_agent_id, mention_count, mentioned_logins[]} — handy for Slack/email integrations that want to alert the mentioned agent in their primary tool.

ticket.snoozed / ticket.unsnoozed fire on snooze state changes. Snooze payload: {id, snoozed_until_utc, actor_agent_id}. Unsnooze payload: {id, actor_agent_id, auto} — auto:true when the lazy auto-unsnooze fires (snooze elapsed on next view), auto:false for manual unsnooze.

ticket.subject_changed fires on inline rename. Payload: {id, from_subject, to_subject, actor_agent_id}.

ticket.tag_added fires when a new tag is appended to a ticket (idempotent — re-adding an existing tag does NOT fire the event). Payload: {id, tag, actor_agent_id}. Useful for tag-driven routing automations (e.g. "tag = vip → page on-call").

GET csat

/v1/api.ashx?resource=csat[&days=N][&limit=M][&score_min=K][&score_max=K][&recent_window_days=N][&format=csv]

Customer-satisfaction loop output. GET returns a window summary (default 30 days, max 365) with average score, response rate, %-rated-4-or-5, and per-bucket distribution counts (1..5), plus the limit most-recent answered surveys (default 50, max 200) including written comments. The CSAT magic-link token is intentionally not returned — it's a customer-facing secret.

Surveys are auto-issued on ticket resolution (gate via csat.auto_issue_on_resolve feature flag); deliver the link via the ticket.csat_issued webhook (payload includes the token for email automation), or react to ticket.csat_responded / ticket.csat_low_score for response-time follow-up.

Optional filters on the recent array (the summary always covers the full window):

ParamNotes
score_minInclusive lower bound 1..5. 0 (default) = no bound.
score_maxInclusive upper bound 1..5. 0 (default) = no bound. score_max=2 is the canonical detractor-only feed.
recent_window_daysIndependent window for the recent[] list. Defaults to days so summary + recent stay in sync; pass a different value when polling a tight window (e.g. recent_window_days=1 for a daily detractor digest).
include=trendAdds a trend array of {date, response_count, avg_score} rows — one per day across the days window. Off by default; set ?include=trend for partner dashboards that want to chart the direction of travel without a second round-trip.
format=csvBlock 238: emits the recent array as text/csv (summary + trend are JSON-only — single-row aggregates aren't useful in CSV). Stable column shape; magic-link token column deliberately excluded.
# Detractor digest: pull every 1-2 score from the last 24h
curl -H "Authorization: Bearer mlc_xxx" \
     "https://www.mylivechat.com/v1/api.ashx?resource=csat&score_max=2&recent_window_days=1&limit=200"

# Promoters with comments — testimonial mining
curl -H "Authorization: Bearer mlc_xxx" \
     "https://www.mylivechat.com/v1/api.ashx?resource=csat&score_min=5&limit=50"

GET ticket_tags

/v1/api.ashx?resource=ticket_tags[&limit=N]

Distinct ticket-tag inventory with usage counts. Different from the curated tags resource (which is the conversation-tag catalog) — this surfaces the free-text tags actually applied across the tenant's tickets. Useful for autocomplete UIs in custom integrations.

GET macros

/v1/api.ashx?resource=macros[&q=...][&limit=N][&format=csv]

Tenant-shared reply snippets. API surface returns only shared macros — personal snippets owned by individual agents are intentionally excluded since the API token is a service account, not a specific agent. Each row has id, title, body_markdown, tags, use_count, created_utc, updated_utc. Authoring stays in /dashboard/macros.ascx.

Block 213: ?format=csv emits the same row set as text/csv with a timestamped filename. Column shape matches the canonical generic-CSV format that macros_import consumes, so an export from one MyLiveChat tenant re-imports cleanly into another tenant — useful for multi-tenant deployments that want to seed a new tenant's reply library from a sibling tenant's curated set.

# Audit the current shared macro library
curl -H "Authorization: Bearer mlc_xxx" \
     "https://www.mylivechat.com/v1/api.ashx?resource=macros&format=csv&limit=200" \
     -o macros-backup.csv

# Re-import into a sibling tenant
curl -X POST -H "Authorization: Bearer mlc_yyy" \
     -H "Content-Type: text/csv" \
     --data-binary @macros-backup.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=macros_import&platform=generic"

GET health

/v1/api.ashx?resource=health

Single-call operational snapshot for external monitors / status-page automations. Returns:

{ "ok": true, "data": {
    "as_of_utc":               "2026-04-29T15:00:00Z",
    "open_tickets":            12,
    "pending_tickets":         3,
    "unassigned_open_pending": 1,
    "snoozed":                 4,
    "stale_no_first_reply":    2,
    "stale_threshold":         "per_priority_sla",
    "webhook_queue_pending":   0,
    "webhook_failed_24h":      0,
    "last_inbound_message_utc":"2026-05-02T01:42:09Z",
    "is_now_within_business_hours": true,
    "business_calendar_active":     true,
    "portal_active_tokens":         312,
    "portal_total_uses":            1284,
    "customer_identities":          4801
}}

stale_no_first_reply counts open/pending tickets that have passed their per-priority response SLA target without a FirstResponseUtc timestamp (defaults are urgent=15min, high=60, normal=240, low=1440 minutes; per-tenant overrides at /dashboard/sla_policy.ascx). Designed to be cheap so a 1-minute Pingdom poll doesn't strain the DB; safe to call as often as your monitor requires.

webhook_queue_pending is the count of outbound deliveries currently queued for retry (a non-trivial number usually means a subscriber endpoint is slow or down). webhook_failed_24h is the count of permanently-failed deliveries in the last 24 hours (alert-worthy — the integration owner needs to fix their endpoint or signature secret).

last_inbound_message_utc is the timestamp of the most recent inbound chat message across all sessions. null for tenants who haven't received any messages yet; used by ops monitors to detect silent downtime (chat widget unable to deliver, integration misrouting) where ticket counts look fine but no new traffic is landing.

Block 232: is_now_within_business_hours + business_calendar_active let external monitors distinguish "stale tickets at noon on a workday" (alert) from "stale tickets at 3am Sunday" (expected). The calendar lookup is wrapped so a misconfigured calendar can't break the health probe; both fields fall back to false on lookup failure. business_calendar_active reflects whether the sla.use_business_calendar flag is on — if off, is_now_within_business_hours is informational only (SLA is still wall-clock).

Block 266: portal_active_tokens + portal_total_uses + customer_identities mirror the same data Block 261 surfaces on migrate_status — letting a single /v1/health call answer both "is this tenant operationally healthy" and "is the customer-facing surface actually being used". Pre-deploy tenants (no portal_token / customer_identity tables yet) get zeroes rather than missing keys, so external dashboards can render a tenant card unconditionally.

GET customer

/v1/api.ashx?resource=customer&email=<addr>
/v1/api.ashx?resource=customer&phone=<e164>

Per-customer rollup keyed on email or phone. Returns aggregate ticket stats (total_tickets, open_tickets, first_seen_utc, last_seen_utc), aggregate CSAT (csat_count, csat_avg), per-channel ticket counts (channel_mix), the last 50 tickets, and the last 50 CSAT responses with comments. Mirrors /dashboard/customer_detail.ascx for external CRM-bridge automations. Either email or phone is required; both are accepted simultaneously and union'd at the SQL level.

Block 252: response now also includes identity — the row from dbo.CustomerIdentity (Section 2 customer-identity table, Block 249) with a stable id partner SDKs can key off as the canonical "who is this person" handle. null on tenants pre-deploy or for first-time visitors with no identity row yet. Foundation for cross-channel customer-history reads once Section 5 channels land — the identity id will accumulate (channel, external_user_id) tuples in v2.

GET articles / article

/v1/api.ashx?resource=articles[&status=visible|draft|hidden][&category=...][&q=...][&limit=N][&include_body=1][&format=csv]
/v1/api.ashx?resource=article&id=N

Knowledge-base articles. Same rows that back the public help center (/help/?sid=...) and the in-widget Help tab. The list endpoint omits the article body for speed; pass include_body=1 for migration scripts that need the full markdown bodies in one round-trip. The single endpoint always includes the markdown body. Full authoring stays in /dashboard/kb_articles.ascx; the only write surface is duplicate (below).

The list response also includes an applied_filters echo of the active {status, category, q, limit} — same shape as /v1/tickets and /v1/audit_log, useful for clients building "next page" links without tracking filter state.

Pass format=csv to receive the same row set (post-filter) as text/csv with a timestamped Content-Disposition filename. 11-column shape: id, title, slug, category, status, use_for_ai, view_count, sort_order, excerpt, created_utc, updated_utc. Combine with include_body=1 to add a 12th body column carrying the full markdown — useful for migration-script verification.

GET kb_categories

/v1/api.ashx?resource=kb_categories[&format=csv]

List of KB article categories. Useful as a sibling read for callers that want to know the valid category catalog before POSTing /v1/articles — saves them having to derive valid names from the article list. Includes both enabled and disabled categories (the dashboard manages enable-state); each row carries the live article_count. Sorted by sort_order ASC, then name ASC. Alias: resource=categories.

Block 219: ?format=csv emits the same row set as text/csv. Drives the spreadsheet-based "merge nearly-empty categories" cleanup workflow via article_count, parallel to the tags CSV from Block 216.

GET PATCH skills

/v1/api.ashx?resource=agent_skills[&agent_id=X]
/v1/api.ashx?resource=ticket_skills&ticket_id=N
/v1/api.ashx?resource=skills

Skills-based routing. Each agent declares a free-text skill list ("billing", "spanish", "tier-2-tech"); each ticket optionally declares required skills. When a ticket is created with required_skills, the auto-assign rotation prefers agents whose declared skills cover every required skill. Falls back to global round-robin if no agent matches — assignment is more important than skill perfection.

# GET — list all rotation agents and their skills
curl -H "Authorization: Bearer mlc_xxx" \
  https://www.mylivechat.com/v1/api.ashx?resource=agent_skills

# GET — single agent
curl -H "Authorization: Bearer mlc_xxx" \
  "https://www.mylivechat.com/v1/api.ashx?resource=agent_skills&agent_id=agent42"

# PATCH — replace agent's full skill list
curl -X PATCH -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "agent42", "skills": ["billing", "spanish", "tier-2-tech"]}' \
  https://www.mylivechat.com/v1/api.ashx?resource=agent_skills

# GET — site's distinct skill catalog (for autocomplete UIs)
curl -H "Authorization: Bearer mlc_xxx" \
  https://www.mylivechat.com/v1/api.ashx?resource=skills

# Create a ticket that needs skill-routing — required_skills field on /v1/tickets POST
curl -X POST -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Spanish-speaking customer billing question",
       "priority": "normal",
       "required_skills": ["billing", "spanish"]}' \
  https://www.mylivechat.com/v1/api.ashx?resource=tickets

Skill names are normalized to lowercase, trimmed, capped at 60 chars, and limited to [a-z0-9 ._-]. The implicit catalog is whatever names appear in any agent or ticket row — admins create a new skill simply by setting it on someone. PATCH replaces the full list (pass "skills": [] to clear).

Routing semantics (computed at POST /v1/tickets time): the rotation candidate list is intersected with agents whose skills cover every required skill. If at least one match exists, round-robin proceeds within that subset (preserving per-cohort fairness). If the intersection is empty, falls back to round-robin over the full rotation list. The required skills are persisted to dbo.TicketSkill post-commit so subsequent re-routes / dashboard reads see them.

GET migrate_status

/v1/api.ashx?resource=migrate_status

Aggregate migration progress across all three importer audit tables in one call — mirrors the data shown on the dashboard /dashboard/migrate.ascx hub. Drives partner CI tools that need to monitor cutover progress without three separate round-trips. Tenants who haven't run any importer yet (or whose deploy SQL hasn't shipped) get zeroed counters and null timestamps, never an error.

# Cutover progress at a glance
curl -s -H "Authorization: Bearer mlc_xxx" \
     "https://www.mylivechat.com/v1/api.ashx?resource=migrate_status" \
| jq '. | "tickets: \(.tickets.imported_rows) imported · \(.tickets.run_count) runs · last \(.tickets.last_run_utc)\narticles: \(.articles.imported_rows) imported · \(.articles.run_count) runs\nmacros: \(.macros.imported_rows) imported · \(.macros.run_count) runs"'

Response shape:

{
  "tickets":  { "imported_rows": 4827, "skipped_rows": 12, "failed_rows": 0,
                "run_count": 3, "last_run_utc": "2026-05-04T08:13:21.230Z" },
  "articles": { "imported_rows": 412,  "skipped_rows": 8,  "failed_rows": 1,
                "run_count": 1, "last_run_utc": "2026-05-03T17:40:08.110Z" },
  "macros":   { "imported_rows": 0,    "skipped_rows": 0,  "failed_rows": 0,
                "run_count": 0, "last_run_utc": null },
  "portal_tokens":       { "active_tokens": 312, "expired_tokens": 4,
                           "revoked_tokens": 0,   "total_uses": 1284 },
  "customer_identities": { "total_identified": 4801, "with_email": 4790,
                           "with_phone": 233 },
  "channel_foundation":  { "total_bound_handles": 1842,
                           "channels": [
                             { "channel": "whatsapp",  "bound_handles": 1402,
                               "msg_inbound_month": 8814, "msg_outbound_month": 4127 },
                             { "channel": "email",     "bound_handles": 318,
                               "msg_inbound_month": 612,  "msg_outbound_month": 411 },
                             { "channel": "sms",       "bound_handles": 122,
                               "msg_inbound_month": 87,   "msg_outbound_month": 31 }
                           ] },
  "ai_usage":            { "calls_month": 14823, "tokens_month": 38219410,
                           "cost_month_usd": 142.81,
                           "errors_month": 12, "cache_hits_month": 9117 }
}

Counts are cumulative across every run for the calling tenant. Re-runs of the same CSV (which dedupe on (site_id, platform, external_id)) inflate skipped_rows rather than imported_rows, so the imported count remains a reliable "actually-landed-in-our-tenant" total.

Block 261: response now also includes portal_tokens + customer_identities rollups so the migrate dashboard shows the FULL cutover state — tickets imported AND customers identified (Section 2 foundation, Block 249) AND portal links minted (Block 244 foundation). Pre-deploy tenants get zeroed sub-objects rather than missing keys, so consumers can render empty states without conditional shape handling.

Block 274: response now also includes channel_foundation — per-channel bound-handle counts plus current-month inbound/outbound message volume from dbo.ChannelMessageVolume. Tenants pre-deploy get an empty channels[] array. Drives the "is this tenant ready for / actively using which channels" view in partner CI dashboards alongside the import counters.

Block 288: response now also includes ai_usage — month-to-date counts + tokens + cost + errors + cache hits from dbo.AiUsageDailyRoll. Same shape as the corresponding ai_usage.summary sub-object. Pre-deploy or pre-gateway-wire-up tenants get zeros across the board. Migrate-status now spans the full operational footprint: ticket / KB / macro imports + portal + identity + channels + AI — one read, no special-casing.

GET agents

/v1/api.ashx?resource=agents[&format=csv]

Active rotation roster — the list of agent ids who actually answer tickets via auto-assignment, with their declared skills inline so setup wizards and partner UIs don't need a second round-trip per agent. Tenants who haven't enabled rotation get an empty items array; same shape, no error. The rotation_cursor + per-row is_next_in_line tell you who gets the next round-robin ticket.

Why this is the right shape vs. listing every dbo.Customer row: the canonical agent identity is the dashboard login id, but the operational agent set — who's expected to handle tickets — lives in dbo.AssignmentRotation. Setup wizards configuring skills or routing care about the operational set, not every login that's ever been provisioned. Use the dashboard /dashboard/assignment.ascx to add or remove agents from the rotation.

{
  "items": [
    {
      "agent_id":          "agent42",
      "rotation_position": 0,
      "is_next_in_line":   false,
      "skills":            ["billing", "spanish"]
    },
    {
      "agent_id":          "agent17",
      "rotation_position": 1,
      "is_next_in_line":   true,
      "skills":            ["tier-2-tech"]
    }
  ],
  "count": 2,
  "rotation_enabled": true,
  "rotation_cursor":  1
}

?format=csv emits a flat shape with skills joined by ; (semicolon, since commas are the field separator). Skills are read via /v1/skills's underlying repository, so any out-of-band edit to dbo.AgentSkill shows up here on the next call.

GET weekly_report

/v1/api.ashx?resource=weekly_report[&format=html]

Pre-rendered weekly summary for tenant-side cron delivery. Covers the trailing 7 days: tickets created/resolved/open/pending, CSAT issued/responded/average/detractor count, the 5 most-recent detractor comments, stale-tickets count, webhook pending/failed-24h. Default JSON for BI ingest; ?format=html emits a compact email-ready body (table layout, inline styles, no JS, no external resources) that pipes cleanly to sendmail / SES SendEmail with Body.Html.

The endpoint is the artifact — scheduling lives on your side. Block 253 deliberately avoids embedding a scheduler in the app since that would touch deploy posture (background workers, retry policies, timezone settings). Tenants on cron + curl + mail get the canonical pattern in one shot; tenants on Zapier / n8n / Make schedule a daily polling step against this URL.

# Cron pattern: Monday 08:00 local, email the report
0 8 * * 1 curl -s -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=weekly_report&format=html" \
| (echo "Subject: Weekly summary — $(date +%Y-%m-%d)"; echo "Content-Type: text/html"; echo; cat) \
| sendmail -t [email protected]

The HTML branch only renders sections that have data: CSAT block hides when zero surveys were issued; "Needs attention" block only appears when there are stale tickets or webhook failures. Quiet weeks produce a short, scannable email; busy weeks fill the page.

GET setup_status / setup_checklist

/v1/api.ashx?resource=setup_status

Tenant configuration completeness checklist (Block 270). Returns an array of {key, label, status, hint} items each probing one configuration concern (deploy SQL applied, calendar configured, webhooks subscribed, rotation populated, ...). Drives setup wizards: partners render the checklist directly as their progress UX without per-feature probe calls.

status is one of:

  • ok — fully configured / nothing more to do
  • missing — not started; click the hint URL
  • partial — started but incomplete (e.g. calendar deploy ran but no rows yet, or webhooks subscription exists but is_enabled=false)
{
  "ok": true, "data": {
    "items": [
      { "key": "webhook_subscriptions",
        "label": "At least one outbound webhook subscription",
        "status": "ok", "hint": "" },
      { "key": "business_calendar",
        "label": "Business calendar configured for SLA",
        "status": "partial",
        "hint": "Calendar rows exist; flip sla.use_business_calendar ON at /dashboard/config_feature_flags.ascx." },
      ...
    ],
    "ok": 5, "missing": 1, "partial": 2, "complete": false
  }
}

The summary counters at the bottom (ok / missing / partial / complete) drive a top-level "8 of 10 done" progress bar without iterating items. Endpoint is cheap (one tiny aggregate read per item) but isn't designed for hot-path polling — it's a tenant-onboarding tool. Probe at setup time and after major config changes; not on every page load.

GET ai_usage / ai_spend

/v1/api.ashx?resource=ai_usage[&days=N][&include=trend][&format=csv]

Per-tenant AI call counts + token volume + cost rollup (Block 280 — Section 4 substrate). Drives the AI cost dashboard, budget-cap alerts, and partner CI scripts that watch tenant AI spend. Schema lives in App_Data/ai_usage_deploy.sql; presence surfaces in whoami.capabilities.deployed.ai_usage_log. Pre-deploy tenants get a zeroed shape — same JSON shape regardless of state, no conditional handling required.

Returns two sub-objects: summary with today / yesterday / month-to-date counts + tokens + USD cost + cache hits + errors, and by_feature[] with a trailing-N-days breakdown per AI feature (default 7, max 90 days). Cache-hit rate and error rate are pre-computed (call-divisor protected) so dashboards don't need to handle divide-by-zero.

# Headline numbers for the "AI spend" topbar card
curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=ai_usage" \
| jq '.summary | "calls today: \(.calls_today) · cost MTD: $\(.cost_month_usd) · errors: \(.errors_month)"'

# Per-feature breakdown for the last 30 days
curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=ai_usage&days=30" \
| jq '.by_feature[] | {feature, calls, tokens, cost_usd, cache_hit_rate}'

Response shape:

{
  "summary": {
    "calls_today":      482,
    "calls_yesterday":  517,
    "calls_month":      14823,
    "tokens_today":     1284732,
    "tokens_month":     38219410,
    "cost_month_usd":   142.81,
    "cache_hits_month": 9117,
    "errors_month":     12
  },
  "by_feature": [
    { "feature": "bot",     "calls": 9214, "tokens": 22418732,
      "cache_hits": 6014, "errors": 4, "cost_usd": 87.32,
      "mean_latency_ms": 1820.4, "cache_hit_rate": 0.652, "error_rate": 0.000 },
    { "feature": "copilot", "calls": 4108, "tokens": 14217902,
      "cache_hits": 2710, "errors": 6, "cost_usd": 51.84,
      "mean_latency_ms": 2410.1, "cache_hit_rate": 0.659, "error_rate": 0.001 },
    { "feature": "summary", "calls": 1501, "tokens": 1582776,
      "cache_hits": 393,  "errors": 2, "cost_usd": 3.65,
      "mean_latency_ms": 980.2,  "cache_hit_rate": 0.262, "error_rate": 0.001 }
  ],
  "applied_filters": { "days": 7 }
}

Allowed feature names (matched on the gateway side): bot, copilot, summary, intent, tone, kb_search. The gateway lands in a future block; this endpoint exists today and returns rows only after the gateway starts writing through AiUsageRepository.LogCall. Until then you get by_feature: [] and zero counters — useful for dashboard scaffolding without waiting on the gateway dependency.

The cost_usd values are best-effort estimates computed at log-time from current per-model rate cards; they're informational, not authoritative. Authoritative tenant billing reconciles against the vendor invoices via the existing AiSiteBilling path.

Block 282: opt-in ?include=trend adds a per-day series of {date, calls, tokens, cost_usd} across the active days window — drives partner-side sparklines + line charts without a second round-trip. Days with zero calls are pre-filled (not omitted), so the array length always equals the requested days value:

{
  "summary": { ... },
  "by_feature": [ ... ],
  "trend": [
    { "date": "2026-04-28", "calls": 482, "tokens": 1284732, "cost_usd": 4.82 },
    { "date": "2026-04-29", "calls": 0,   "tokens": 0,       "cost_usd": 0    },
    { "date": "2026-04-30", "calls": 517, "tokens": 1411208, "cost_usd": 5.17 }
  ],
  "applied_filters": { "days": 7, "include": "trend" }
}

The same series powers the inline SVG sparkline on the dashboard's /dashboard/ai_usage.ascx page (Block 281).

Block 286: ?format=csv emits the same by_feature row set as text/csv with a timestamped Content-Disposition filename and UTF-8 BOM. Drives compliance / finance ingest pipelines that want a flat, spreadsheet-friendly snapshot of AI spend per feature. Header columns: feature, calls, tokens, cache_hits, errors, cost_usd, mean_latency_ms, cache_hit_rate, error_rate. The summary + trend sub-objects are not included in CSV — use the JSON endpoint for those.

GET PATCH ai_limits / ai_guardrails

/v1/api.ashx?resource=ai_limits

Per-tenant AI guardrails (Block 283 — Section 4 layer above the usage substrate). Returns the current cap state for three independent caps: budget (monthly USD), daily_tokens, and minute_calls (RPM). Null limit = no cap configured (the default; tenant behaves as before). The future gateway calls AiUsageGuardrails.CheckAll internally before issuing any vendor request and either short-circuits (action=hard) or proceeds while logging status=budget_exceeded (action=soft, the default).

Cap configuration is stored as additive columns on dbo.AiSiteBilling: MonthlyBudgetUsd, DailyTokenLimit, MinuteCallLimit, BudgetExceededAction. All caps default NULL — strictly opt-in. Tenants who never set caps are unaffected.

# Watch for tenants approaching their budget cap
curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=ai_limits" \
| jq 'select(.budget.percent_used > 80) | "WARN: tenant at \(.budget.percent_used)% of $\(.budget.limit)"'

Response shape:

{
  "budget": {
    "limit":        500.00,
    "current":      142.81,
    "exceeded":     false,
    "headroom":     357.19,
    "percent_used": 28.6
  },
  "daily_tokens": {
    "limit":        null,            # no cap configured
    "current":      1284732,
    "exceeded":     false
  },
  "minute_calls": {
    "limit":        60,
    "current":      12,
    "exceeded":     false,
    "headroom":     48,
    "percent_used": 20.0
  },
  "any_exceeded":   false,
  "action":         "soft"            # soft|hard|none
}

The three caps are evaluated independently; any_exceeded is true if any of them is exceeded. action = none when no caps are configured (gateway skips guardrail logic entirely on these tenants — the read is informational only). action = soft means the gateway logs status=budget_exceeded in AiUsageEvent but allows the vendor call; admins see the spike in the ai_usage errors counter without breaking customer-facing flows. action = hard means the gateway returns a budget_exceeded error before the vendor call and per-feature fallback (bot → KB-search-only, copilot → disabled) takes over.

Block 287: PATCH endpoint for setting caps. Each field is independent — omit to leave unchanged, pass null to clear:

# Set monthly budget + soft action; leave token + RPM caps untouched
curl -X PATCH -H "Authorization: Bearer $MLC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"monthly_budget_usd": 500.00, "action": "soft"}' \
  "https://www.mylivechat.com/v1/api.ashx?resource=ai_limits"

# Clear the budget cap (let tenant burn unlimited)
curl -X PATCH -H "Authorization: Bearer $MLC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"monthly_budget_usd": null}' \
  "https://www.mylivechat.com/v1/api.ashx?resource=ai_limits"

# Switch to hard enforcement once cost-controlled trial ends
curl -X PATCH -H "Authorization: Bearer $MLC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "hard"}' \
  "https://www.mylivechat.com/v1/api.ashx?resource=ai_limits"

Response is identical to GET /v1/ai_limits — the post-patch state, so callers don't need a follow-up read. Validation: budget non-negative, integers non-negative, action ∈ {soft, hard}. At least one field must be present (else 400 empty_patch). Tenants without an AiSiteBilling row get one upserted automatically; ops no longer needs to seed billing rows manually.

GET self_test / ai_usage_self_test / channel_self_test

/v1/api.ashx?resource=self_test

Substrate post-deploy verification (Blocks 293, 294, 295). Three endpoints layered for different CI use cases:

  • self_test (composite, alias self_test_all) — runs every substrate self-test sequentially and returns a rolled-up shape with per-test ok / error / round_trip_ms. Partner CI's one-curl "is everything deployed and healthy" probe.
  • ai_usage_self_test (alias ai_self_test) — writes a synthetic _self_test row via AiUsageRepository.LogCall + reads back the rollup counter. Verifies schema deployed AND write/read paths agree. Idempotent — re-running bumps the segregated counter, never leaks into real metrics.
  • channel_self_test (alias channel_foundation_self_test) — read-only. Verifies all 5 schema components (3 Ticket columns + ChannelMessageVolume + CustomerIdentityChannel) and exercises GetByExternalUser against a UUID-suffixed sentinel that can't collide with real handles.
# Composite — partner CI gate after running deploy SQL
curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=self_test"

Composite response shape:

{
  "ok":               true,
  "total_runtime_ms": 23,
  "tests": [
    { "name": "ai_usage",            "ok": true, "schema_deployed": true,
      "pre_calls": 42, "post_calls": 43, "round_trip_ms": 12, "error": null },
    { "name": "channel_foundation", "ok": true,
      "components_present": 5, "components_total": 5,
      "round_trip_ms": 8, "error": null }
  ]
}

Overall ok is the AND of every sub-test's ok — partner CI gates can branch on a single boolean. Per-test failures surface a free-form error string and the round-trip latency so you can diagnose schema-not-deployed vs. permissions-issue vs. perf-regression.

Designed to run on every deploy as part of the post-flight check — self_test's total round-trip is typically <30ms, so adding it to a CI pipeline costs nothing meaningful. The endpoint naturally extends as new substrates land: each new deploy SQL gets its own self-test, registered in the composite handler, partner CI sees the additional probe in the next response without code changes.

GET customer_identities / customers

/v1/api.ashx?resource=customer_identities[&limit=N][&skip=M][&order_by=tickets|recent|name][&format=csv]

List the tenant's customer-identity rows (Section 2 surface; Block 249 schema). Default order is most-engaged first (order_by=tickets) — the typical CRM enrichment ask. order_by=recent flips to last-seen-first for "recently active" segments; order_by=name for alphabetic browsing. Each row carries {id, email, phone, display_name, ticket_count, first_seen_utc, last_seen_utc}; the id is the stable cross-channel handle that the customer endpoint also returns.

# Top 25 most-engaged customers as CSV for HubSpot upload
curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=customer_identities&limit=25&format=csv"

# Recent-first for CS handoff briefings
curl -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=customer_identities&order_by=recent&limit=10"

Pre-deploy tenants get an empty items array rather than a 500 — the table is missing, so there's nothing to list. Run customer_identity_deploy.sql + customer_identity_backfill.sql to populate; ongoing creates are wired into TicketRepository.Create automatically.

POST DELETE bind_channel / channel_bind / identity_channel

/v1/api.ashx?resource=bind_channel

Bind an external (channel, external_user_id) handle to a customer identity (Block 274). Idempotent — calling twice with the same triple refreshes last_seen_utc and returns ok=true. Backed by the Section 2 dbo.CustomerIdentityChannel junction table from App_Data/channel_foundation_deploy.sql; pre-deploy tenants get a 500 bind_failed with a hint pointing to the deploy script.

Identity resolution order: explicit identity_id in the body wins; otherwise email is looked up via /v1/customer's underlying probe; otherwise phone; otherwise a fresh identity row is minted from the email/phone supplied. At least one of identity_id, email, or phone must be present, else 400 no_identity.

Allowed channel values: whatsapp, messenger, instagram, sms, email. Anything else is rejected with 400 bad_field to prevent typo-driven fragmentation (e.g. whats_app vs whatsapp).

# Pre-bind a WhatsApp handle for a known customer (partner workflow)
curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]",
       "channel":"whatsapp",
       "external_user_id":"wa+15551234567",
       "display_handle":"Alice (WhatsApp)"}' \
  "https://www.mylivechat.com/v1/api.ashx?resource=bind_channel"

# Bind by explicit identity_id (after enrichment)
curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"identity_id":4231,
       "channel":"messenger",
       "external_user_id":"PSID-9876543210"}' \
  "https://www.mylivechat.com/v1/api.ashx?resource=bind_channel"

Response:

{
  "ok": true,
  "identity_id": 4231,
  "channel": "whatsapp",
  "external_user_id": "wa+15551234567",
  "display_handle": "Alice (WhatsApp)"
}

Use cases: (1) partner pre-populates handles before launch so the first inbound webhook threads into a known identity rather than minting an orphan that has to be merged later; (2) manual stitching during agent investigation ("this WhatsApp number is the same person as this email"); (3) bulk import after migrating from a platform that stored channel handles separately.

Verify via /v1/customer?email=… — the response now includes identity.channels[] with each bound handle's {channel, external_user_id, display_handle, first_seen_utc, last_seen_utc}. The agent dashboard surfaces these as a "Reachable on:" pill row on customer_detail.ascx.

DELETE the binding when a customer changes number, after an identity merge, or for GDPR-style sweeps. Idempotent — already-absent rows return ok=true:

# Unbind a stale WhatsApp number
curl -X DELETE -H "Authorization: Bearer $MLC_TOKEN" \
  "https://www.mylivechat.com/v1/api.ashx?resource=bind_channel&channel=whatsapp&external_user_id=wa+15551234567"

No webhook event fires on unbind — the inverse signal is rare enough that consumers who care can detect via the absence of the handle in /v1/customer's identity.channels[].

POST bind_channels_bulk / channel_bind_bulk / import_channel_handles

/v1/api.ashx?resource=bind_channels_bulk[&dry_run=1]

Bulk-bind channel handles from a CSV body (Block 278). Mirrors the established tickets_import / kb_import / macros_import shape: POST a CSV up to 16 MB, get summary counters + first-5 error sample, optionally ?dry_run=1 for preview without writes. Per-row idempotent — re-running the same export inflates refreshed instead of bound.

CSV header (column order doesn't matter; extra columns ignored):

channel,external_user_id,email,display_handle,phone

Required: channel, external_user_id. Optional: email, display_handle, phone. Per-row identity resolution: email lookup → phone lookup → mint a fresh identity from email/phone (or, for sms/whatsapp, the external_user_id itself). Rows that resolve to no identity (no email, no phone, channel is messenger/instagram/email) bump no_identity in the summary and surface in the error sample.

Allowed channel values: whatsapp, messenger, instagram, sms, email. Anything else bumps bad_channel.

# Dry-run preview — no writes, see what would happen
curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
  -H "Content-Type: text/csv" \
  --data-binary @channels.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=bind_channels_bulk&dry_run=1"

# Live — bind everything in the file
curl -X POST -H "Authorization: Bearer $MLC_TOKEN" \
  -H "Content-Type: text/csv" \
  --data-binary @channels.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=bind_channels_bulk"

Response shape:

{
  "total_rows":      1842,
  "bound":           1718,    # freshly inserted
  "refreshed":       104,     # already-existing rows had LastSeenUtc bumped
  "minted_identity": 312,     # identity rows created from email/phone keys
  "bad_channel":     0,
  "no_identity":     20,      # rows skipped — no resolvable identity
  "failed":          0,
  "error_sample":    [ "Row 1841: no identity (provide email or phone...)" ],
  "dry_run":         false
}

No identity.channel_bound webhook event fires per row — at bulk-import scale (10k+ rows) the fan-out would melt the delivery queue. Partners who need event-driven mirroring should use single-row POST /v1/bind_channel, which fires the event on every call.

Verification path after import: GET /v1/migrate_status returns channel_foundation.total_bound_handles + per-channel breakdown, so partner CI dashboards see the bulk-bind result in the same surface as ticket / KB / macro imports.

GET PATCH business_calendar

/v1/api.ashx?resource=business_calendar[&format=csv]

Per-tenant business calendar that drives calendar-aware SLA computation (Section 8 v1, Block 223). The calendar is consulted by SlaService.ComputeBreach when the sla.use_business_calendar flag is on; with the flag off (default), SLA continues using wall-clock minutes regardless of what's in this table. Times are stored in UTC, as minutes since midnight.

Defaults: when no rows exist for a tenant, a synthesized Mon–Fri 09:00–17:00 UTC week is returned so the SLA badges keep rendering before the tenant configures anything. Once a tenant POSTs at least one row, the synthesized defaults disappear — partial configurations represent themselves.

# Read the active calendar + upcoming holidays + flag state
curl -H "Authorization: Bearer mlc_xxx" \
  https://www.mylivechat.com/v1/api.ashx?resource=business_calendar

# Replace the week (24/7 support example: 0..1440 every day)
curl -X PATCH -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"week":[
    {"day_of_week":0,"open_minute":0,"close_minute":1440},
    {"day_of_week":1,"open_minute":0,"close_minute":1440},
    {"day_of_week":2,"open_minute":0,"close_minute":1440},
    {"day_of_week":3,"open_minute":0,"close_minute":1440},
    {"day_of_week":4,"open_minute":0,"close_minute":1440},
    {"day_of_week":5,"open_minute":0,"close_minute":1440},
    {"day_of_week":6,"open_minute":0,"close_minute":1440}]}' \
  https://www.mylivechat.com/v1/api.ashx?resource=business_calendar

# Add a fixed-date holiday
curl -X POST -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"date_utc":"2026-12-25","label":"Christmas Day"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=business_calendar_holiday

# Block 227: bulk-add a preset bundle (US federal / UK bank / EU common)
# for the given year. Idempotent — already-present dates are skipped.
curl -X POST -H "Authorization: Bearer mlc_xxx" \
  "https://www.mylivechat.com/v1/api.ashx?resource=business_calendar_holiday&preset=us_federal&year=2026"

# Remove a holiday by id
curl -X DELETE -H "Authorization: Bearer mlc_xxx" \
  "https://www.mylivechat.com/v1/api.ashx?resource=business_calendar_holiday&id=42"

Response shape (GET):

{
  "week": [
    {"day_of_week":0,"open_minute":0,   "close_minute":0,   "is_closed":true },
    {"day_of_week":1,"open_minute":540, "close_minute":1020,"is_closed":false},
    ...
  ],
  "upcoming_holidays": [
    {"id":42,"date":"2026-12-25","label":"Christmas Day"}
  ],
  "flag_on": false,
  "is_now_within_hours": true,
  "minutes_until_next_change": 95
}

Block 229: is_now_within_hours + minutes_until_next_change let partner UIs render an "open / opens in 95 min" chip without re-implementing the calendar logic. Both are best-effort — on a calendar query failure they fall back to false / 0 respectively rather than failing the whole GET. minutes_until_next_change caps at 7 days (10080) for permanently-closed tenants.

Time encoding: open_minute / close_minute are 0..1440 minutes since midnight UTC. Closed days have open_minute >= close_minute (the canonical encoding is 0/0). Repository validation clamps out-of-range values + auto-corrects open > close rather than rejecting, so a typo can't lock the admin out of saving. Multi-window days (siesta, lunch break) are deferred to v2 — single-window-per-day covers every tenant we've seen so far. Tenant-local time-zone awareness is also deferred to v2; UTC handles the overwhelmingly common case where tenant + customers are in the same TZ. ?format=csv emits the week as text/csv (no holidays — that list is shaped for JSON consumers). Requires read for GET, write for PATCH/POST/DELETE.

GET PATCH feature_flags

/v1/api.ashx?resource=feature_flags[&format=csv]

Per-tenant feature toggles — controlled rollout for any flagged feature without touching code or invalidating cached pages. Each flag has a system-wide default; tenants can override via the dashboard /dashboard/config_feature_flags.ascx or via this API. Reads are cached AppDomain-locally for 60 seconds. Alias: resource=flags. Block 221: ?format=csv emits the snapshot for config-as-code / Terraform-style flows — drop in git, diff per release, reapply via PATCH.

# GET — list all known flags + this site's effective state
curl -H "Authorization: Bearer mlc_xxx" \
  https://www.mylivechat.com/v1/api.ashx?resource=feature_flags

# PATCH — override (true / false / null=clear)
curl -X PATCH \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"key": "calls.video", "enabled": false, "notes": "Disabled while we sort out TURN config"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=feature_flag

Each row carries: key (stable dotted identifier), default (system-wide), site_value (override or null), effective (what IsEnabled() returns today), description, notes.

Initial flag catalog: calls.voice, calls.video, tickets.bulk_ui, tickets.merge_ui, kb.ai_search, ai.copilot, webhooks.token_events. New flags get registered in FeatureFlagRepository.Defaults — PATCH on an unregistered key returns 404 unknown_flag.

Updates write updated_by = "api:<token-prefix>" for SOC2 audit. The dashboard page writes updated_by = "agent:<userid>".

GET analytics

/v1/api.ashx?resource=analytics[&days=N]

Headline analytics counters across the active window (default 7 days, max 90). Returns {conversations, tickets_created, tickets_resolved, ai_handoffs, kb_searches, kb_searches_no_results, kb_article_views, kb_helpful_yes, kb_helpful_no}. Mirrors the top cards on /dashboard/analytics.ascx.

Note the field names retain their Last7 heritage in the underlying DTO; the values reflect whatever window days selects.

GET analytics_series

/v1/api.ashx?resource=analytics_series&metric=<name>[&days=N]

Per-day series for a single metric over the active window (default 30 days, max 90). The result is zero-padded so consumers can render a contiguous x-axis without their own date math. Each point carries {day (YYYY-MM-DD UTC), count, sum, avg}sum and avg are null for metrics that don't carry a numeric value.

Allowed metric values: conversation_started, conversation_resolved, message_sent, ticket_created, ticket_resolved, ticket_first_response, ai_response_generated, ai_handoff, kb_searched, kb_search_no_results, kb_article_viewed, kb_article_helpful_yes, kb_article_helpful_no. Anything else returns 400 bad_request.

GET analytics_no_result_searches

/v1/api.ashx?resource=analytics_no_result_searches[&days=N][&limit=N]

Most-frequent KB search queries that returned zero results. Drives content-gap planning — the queries here are the article titles a content writer should consider next. Default window 30 days (max 365); default limit 20 (max 200). Returns {queries: [{query, count}, ...]} sorted by count DESC.

GET analytics_agents

/v1/api.ashx?resource=analytics_agents[&days=N][&limit=N]

Per-agent scorecard. Default window 14 days (max 365); default limit 25 (max 100). Each row carries {agent_id, replies, first_responses, resolutions, avg_first_response_seconds}; the avg is null when the agent has no first-response events in the window. Sorted by total activity (replies + first_responses + resolutions) DESC.

GET analytics_priority_resolution

/v1/api.ashx?resource=analytics_priority_resolution[&days=N]

Average resolution time bucketed by ticket priority. Default window 30 days (max 365). Returns one row per priority that has resolved tickets in the window: {priority, resolved_count, avg_resolution_seconds}. Order is fixed at urgent → high → normal → low so dashboards can render columns without sorting.

GET analytics_channels

/v1/api.ashx?resource=analytics_channels[&days=N]

Tickets created vs. resolved per channel. Default window 30 days (max 365). Each row: {channel, tickets_created, tickets_resolved}. Channels with no activity in the window are omitted; sorted by tickets_created DESC.

GET sla_breaches

/v1/api.ashx?resource=sla_breaches[&state=breached|warning|warning_or_worse][&limit=N]

Currently breaching (or near-breach) open and pending tickets, computed live against each ticket's priority policy. Drives external monitoring and on-call paging integrations — poll once a minute and alert on any state=breached row.

Default state=warning_or_worse (the union most alerting pipes want); limit defaults to 50 (max 200). Sorted by worst_minutes_remaining ascending so the most-overdue ticket is first.

Each row: {id, ticket_number, subject, status, priority, channel, assignee_agent_id, requester_name, requester_email, created_utc, first_response_utc, response_state (ok|warning|breached), response_minutes_remaining, resolution_state, resolution_minutes_remaining, worst_minutes_remaining}. Negative *_minutes_remaining means past target; positive means inside the SLA window.

GET mentions

/v1/api.ashx?resource=mentions&login=<loginname>[&unread_only=1][&limit=N]
/v1/api.ashx?resource=mentions&agent_id=<userid>[&unread_only=1][&limit=N]

Sync-friendly read of @-mentions for a specific agent (the API token is a service account, so the caller must specify whose mentions). Either login or agent_id is required. Each row carries {id, recipient_login, recipient_agent_user_id, author_*, ticket_id, comment_id, context_snippet, is_read, created_utc, read_utc}. Response also includes a top-level unread_count for badge-rendering. Use this as a poll alternative or backstop to the ticket.mentioned webhook.

GET kb_feedback

/v1/api.ashx?resource=kb_feedback[&min_votes=N][&limit=M]

"Needs revision" KB articles — those with the worst helpful-rate among articles that have received at least min_votes total votes (default 3, max limit 50). Each row includes {article_id, title, up_votes, down_votes, total_votes, helpful_pct}. Useful for periodic content-quality scans that flag articles whose visitors are unhappy.

GET article_versions

/v1/api.ashx?resource=article_versions&article_id=N[&limit=M][&include_body=1]

Per-article version history. Returns the most-recent versions descending. Each row carries {id, version_number, title, slug, category, status, use_for_ai, excerpt, editor_*, change_note, created_utc}. Pass include_body=1 for content-migration scripts that need full markdown. Useful for content-audit pipelines that mirror revisions into an external archive.

POST duplicate_article

/v1/api.ashx?resource=duplicate_article

Clone an article as a new draft. Body: {"source_id":N}. Title prefixed "Copy of ", body / category / excerpt copied, status forced to draft, view count reset, slug not carried (slugs must stay unique). Useful for templating: keep a single "FAQ template" article private and POST-clone it as the starting point for new content. Requires write scope.

curl -X POST -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"source_id":42}' \
     https://www.mylivechat.com/v1/api.ashx?resource=duplicate_article

GET POST sla_policy

/v1/api.ashx?resource=sla_policy[&format=csv]

First-response and resolution targets per priority. GET returns all four entries (urgent / high / normal / low) — defaults are inlined when no per-tenant row exists yet. POST upserts; body can be a single entry or an array of entries. Minutes are clamped to [1, 43200] (≤30 days). Requires write scope.

Block 231: ?format=csv emits the policies as text/csv. Completes the config-as-code trio with feature_flags?format=csv and business_calendar?format=csv — three curl calls give a full SLA-relevant config snapshot for git-based change tracking.

curl -X POST -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '[{"priority":"urgent","response_target_minutes":15,"resolution_target_minutes":120},
          {"priority":"high","response_target_minutes":60,"resolution_target_minutes":480}]' \
     https://www.mylivechat.com/v1/api.ashx?resource=sla_policy

The breach badges on /dashboard/tickets.ascx reflect changes on the next page load — no separate cache.

GET usage

/v1/api.ashx?resource=usage

AI usage rollup: total replies, replies in the current cycle, plan code, included replies, and the cycle window. The same numbers shown on /dashboard/config_ai_usage.ascx.

GET feedback

/v1/api.ashx?resource=feedback[&limit=N][&since=ISO][&format=csv]

Recent chat-end feedback rows from dbo.ChatFeedBack — the counterpart to csat for chat-only sessions (CSAT covers ticket resolution surveys; feedback covers the post-chat rating widget). Block 243: ?format=csv emits with stable column shape including ip_address + user_agent, useful for GDPR-style data exports.

GET time_log

/v1/api.ashx?resource=time_log&ticket_id=N
/v1/api.ashx?resource=time_log[&days=N]

Two modes: per-ticket entry list (when ticket_id is provided) or per-agent rollup across a window (default 30 days). Per-ticket response: {ticket_id, total_minutes, entries:[{id, agent_id, agent_name, minutes, note, logged_utc}, ...]}. Rollup response: {window_days, total_minutes, agents:[{agent_id, agent_name, total_minutes, entry_count, tickets_logged_count}, ...]}. Useful for billing exports and per-agent productivity reporting.

GET saved_views

/v1/api.ashx?resource=saved_views[&format=csv]

List of shared saved ticket views. Personal saved views (owned by a specific agent) are intentionally excluded — the API token is a service account, not an agent. Each row includes {id, title, query_string, is_pinned, use_count, created_utc, updated_utc}. Useful for setup wizards / Terraform providers that mirror saved views into a managed config layer.

Block 218: ?format=csv emits the same row set as text/csv with a timestamped filename. Backup or replicate-into-sibling-tenant pattern: snapshot here, re-create per row via POST /v1/saved_views.

GET audit_log

/v1/api.ashx?resource=audit_log[&days=N][&src=ticket|webhook|kb][&event=...][&actor=...][&limit=N][&format=csv]

Public REST surface for the merged audit feed. Pulls from dbo.WebhookDelivery, dbo.TicketActivity, and dbo.KbArticleVersion. Default window 7 days, max 90; result cap is caller-tunable via limit (default 500, max 1000). Each item includes {source, when_utc, ...} with source-specific extras (e.g. event_type, status, last_http_code for webhook rows; ticket_id, actor_*, event_type, from, to for ticket rows). Mirrors /dashboard/audit_log.ascx for compliance integrations.

The event and actor query params are case-insensitive substring filters applied post-merge. actor matches against actor_id / actor_name / editor — useful for "what did Pat do this week?" compliance queries.

Pass format=csv to receive the same row set (post-filter, post-limit) as text/csv with a timestamped Content-Disposition filename. Stable 12-column shape across all sources: when_utc,source,event_type,status,actor_id,actor_name,subscription_id,ticket_id,article_id,from,to,note. Cells are double-quoted RFC 4180 style so commas / newlines / quotes inside notes don't break downstream parsers.

GET webhook_events

/v1/api.ashx?resource=webhook_events

Static catalog of all known outbound webhook event names plus a brief payload-shape hint per event. Setup wizards and Terraform providers consume this to render event-checkbox pickers without hard-coding the list. The wildcard form ticket.* is valid in subscriptions but listed separately under wildcards since it's a meta-token rather than a real event.

GET webhook_subscriptions

/v1/api.ashx?resource=webhook_subscriptions[&event=handoff][&format=csv]

List your outbound HMAC-signed webhook subscriptions. Optional event query filter narrows to subscriptions whose Events CSV includes that event. The full secret is never returned through GET; only secret_prefix (first 8 chars of base64) is exposed.

Block 220: ?format=csv emits the same row set as text/csv. Drives SOC 2 / compliance audit pipelines that need a periodic snapshot of every active outbound integration — who's subscribed to what events, with what secret-rotation status. Like the JSON shape, the CSV never contains the full secret, only the 8-char prefix.

GET webhook_deliveries

/v1/api.ashx?resource=webhook_deliveries[&status=delivered|pending|failed][&subscription_id=N][&days=N][&limit=N][&format=csv]

Tenant-wide delivery log across all subscriptions, sorted newest-first. The dashboard surface (config_webhooks.ascx) shows this per-subscription only — admins debugging a cross-integration outage have to expand each subscription separately. This endpoint flattens the view for ops tooling and unified-timeline integrations.

Block 230: ?format=csv emits the same row set as text/csv with a timestamped filename. Pairs with the existing webhook_subscriptions CSV (Block 220) for a complete "outbound integrations posture" snapshot suitable for SOC 2 compliance audits.

Defaults: days=7 (max 90), limit=100 (max 500). Each row carries {id, subscription_id, subscription_label, event_type, status, attempts, last_http_code, last_error, created_utc, completed_utc, next_retry_utc}. Payload bodies are deliberately not included — they can be 64 KB each, and a tenant-wide list response with payloads in line would blow up the rate budget. Per-row payload reads should use the existing per-subscription dashboard panel.

The response also carries a by_status aggregate over the same window (counts delivered / pending / failed / total). It's computed against the full result set, not the row-capped items array, so dashboards see a tenant-true status breakdown even when limit=100.

Alias: resource=deliveries.

GET visitors

/v1/api.ashx?resource=visitors[&limit=N&offset=M][&format=csv]

Browse visitor CRM rows (the identities your widget's mylivechat('identify',...) SDK call has stored). Returns visitor_key, email, name, plan, custom attributes, last_seen_utc, event_count.

Block 235: ?format=csv emits the same row set as text/csv with a timestamped filename. Drives GDPR data-portability exports and CRM enrichment pipelines that want a flat snapshot of identified visitors. Custom attributes are emitted as a compact JSON string per row in the attributes_json column — this keeps the column shape stable across tenants (which would otherwise have wildly different attribute key sets) at the cost of one extra JSON parse on the consumer side.

GET slack_settings / discord_settings / teams_settings

/v1/api.ashx?resource=slack_settings

Read the configured webhook + event filter for any of the three chat-platform notifiers. The webhook URL is treated as a secret — only webhook_url_present + a short webhook_url_prefix are returned.

{ "ok": true, "data": {
    "platform": "slack",
    "webhook_url_present": true,
    "webhook_url_prefix": "https://hooks.slack.com/services...",
    "events": ["handoff","feedback"]
}}

POST tickets

/v1/api.ashx?resource=tickets

Create a ticket from scratch. The single most-requested write path: contact-form backends, email gateways, mobile apps, and "if X then create ticket" automations all hit this. Routes through the same path as a dashboard creation — round-robin auto-assign (when assignee_agent_id is omitted), the ticket.created webhook, and ticket_created analytics fire identically.

# Minimal create — auto-assigns via rotation, defaults to open/normal/web
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Refund request from Jane"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=tickets
# Full create with requester + initial message + tags
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Login broken on Safari",
       "priority": "high",
       "channel": "email",
       "requester_name":  "Jane Doe",
       "requester_email": "[email protected]",
       "tags": "login,safari",
       "initial_comment": "I tried to log in three times this morning and got the spinner forever..."}' \
  https://www.mylivechat.com/v1/api.ashx?resource=tickets

Body fields:

FieldRequiredNotes
subjectyesCapped at 300 chars.
statusnoDefault open. One of open, pending, resolved, closed.
prioritynoDefault normal. One of low, normal, high, urgent.
channelnoDefault web. One of web, whatsapp, messenger, instagram, sms, email, imported.
requester_name / requester_email / requester_phonenoIdentifies the human the ticket is on behalf of.
assignee_agent_idnoIf omitted, round-robin rotation picks an agent.
conversation_session_idnoLink to a chat session that originated the ticket.
imported_from / imported_external_idnoFor migration tooling — preserves the source system's id.
tagsnoComma-separated, capped at 500 chars total.
author_id / author_namenoActivity-feed attribution. Default api:<token-prefix>.
initial_commentnoIf present, immediately calls AddComment as the requester — saves a second round-trip when ingesting an email-style payload.

Returns the freshly-created ticket row plus initial_comment_id (or null when no initial comment was provided).

PATCH ticket

/v1/api.ashx?resource=ticket

Update one or more of status, priority, assignee (alias assignee_agent_id) on an existing ticket. Each field is optional — only present keys mutate. The actor is recorded as api:<token-prefix> in the activity feed and webhook payloads, so ops can identify which integration made the change.

# Resolve a ticket
curl -X PATCH \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"id": 4711, "status": "resolved"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket
# Reassign + bump priority + reopen, all in one call
curl -X PATCH \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"id": 4711,
       "assignee": "agent42",
       "assignee_name": "Pat Q.",
       "priority": "urgent",
       "status": "open"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket

Returns the post-mutation ticket row plus a fields_changed array listing which fields actually moved (idempotent calls return an empty array). Status transitions are validated — e.g. closed → open returns 409 invalid_transition. Each accepted change fires the same ticket.status_changed / ticket.resolved / etc. webhooks as a dashboard mutation, and writes a row to the ticket activity feed.

POST ticket_reply

/v1/api.ashx?resource=ticket_reply

Add a comment (public reply or internal note) to an existing ticket. Routes through the same path as a dashboard reply — first-response timestamp, ticket_first_response/ticket_replied/ticket_requester_replied analytics, and the ticket.replied / ticket.requester_replied webhooks all fire identically.

# Public agent reply
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_id": 4711,
       "body_markdown": "Thanks — investigating now."}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket_reply
# Internal note (agents-only; never delivered to the requester)
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_id": 4711,
       "internal": true,
       "body_markdown": "Pinged @oncall in #ops, ETA 10m."}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket_reply

Body fields:

FieldRequiredNotes
ticket_id (alias id)yesMust belong to the calling site (cross-tenant returns 404).
body_markdown (alias body)yesServer caps at 64 KB.
internalnoDefault false. true requires author_type=agent.
author_typenoDefault agent. Allowed: agent, system, requester.
author_id / author_namenoDefault to api:<token-prefix> / API (<token-prefix>). Pass your own values to attribute to a specific human.
attachments_jsonnoFree-form JSON string, capped at 16 KB.

POST ticket_merge

/v1/api.ashx?resource=ticket_merge

Merge one ticket (the source) into another (the target). Routes through the same path as a dashboard merge — comments and activity move atomically, both an activity row and the ticket.merged outbound webhook fire, and the source ticket is marked merged-into so subsequent merge attempts return 409 conflict.

# By internal id
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"source_ticket_id": 4711, "target_ticket_id": 4711201}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket_merge

# By human-friendly ticket number (saves a lookup)
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"source_ticket_number": 1042, "target_ticket_number": 1051}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket_merge

Body accepts either {source_ticket_id, target_ticket_id} or {source_ticket_number, target_ticket_number} (or any mix). Common errors:

HTTPCodeWhen
400bad_requestsource == target, or both id fields missing.
404not_foundEither ticket is not in the calling site.
409conflictSource already merged previously, or target is closed.

Returns {merged, source_ticket_id, target_ticket_id, target} where target is the post-merge target ticket row.

POST tickets_bulk

/v1/api.ashx?resource=tickets_bulk

Apply one operation across many tickets in a single call. Each ticket is processed independently — partial failure produces a per-row report rather than aborting the whole batch, since states like "tag was already on the ticket" are normal. Hard cap: 200 ids per call.

# Resolve a list of stale tickets
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_ids": [4711, 4712, 4733, 4801],
       "action":     "resolve"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=tickets_bulk

# Reassign a batch + add a triage tag
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_numbers": [1042, 1043, 1051],
       "action":         "assign",
       "value":          "agent42",
       "assignee_name":  "Pat Q."}' \
  https://www.mylivechat.com/v1/api.ashx?resource=tickets_bulk

Body fields:

FieldRequiredNotes
ticket_ids or ticket_numbersyesArray of internal ids OR human-friendly ticket numbers. ≤200 entries.
actionyesOne of: resolve, close, reopen, assign, priority, add_tag. Bulk delete is intentionally excluded.
valueaction-dependentFor assign: agent id (empty string = unassign). For priority: low/normal/high/urgent. For add_tag: the tag string (no commas). Ignored for status actions.
assignee_namenoFriendly name for the activity feed when action=assign.

Returns {action, total, changed, noop, failed, results}. Each results entry: {id, ok, changed, error?}. Tickets not in the calling site appear as error: "not_found"; invalid status transitions appear as error: "invalid_transition: ...". Successful row mutations fire the same activity rows + outbound webhooks as a single-ticket call.

POST tickets_import

/v1/api.ashx?resource=tickets_import

Bulk-import tickets from a Zendesk, Freshdesk, or LiveAgent CSV export. The endpoint takes the CSV body directly (not wrapped in JSON) so partner integrations and shell scripts don't have to base64-encode just to call it. Idempotent on (site_id, platform, external_id): re-running the same export is a no-op for previously-imported rows, so incremental migrations are safe.

Always pair the first call with ?dry_run=1 to get a parsed-but-not-committed preview. The response includes counts and up to 10 sample rows so you (or your migration script) can confirm the column mapping looks right before flipping dry_run off.

# Step 1: dry-run to preview
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: text/csv" \
  --data-binary @zendesk-export.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=tickets_import&dry_run=1"

# Step 2: commit (omit dry_run, optionally pin platform)
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: text/csv" \
  --data-binary @zendesk-export.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=tickets_import&platform=zendesk&file=zendesk-export.csv"

Query parameters:

ParamRequiredNotes
platformnoOne of zendesk, freshdesk, liveagent. Default auto — detect from header columns. Auto-detect needs at least 3 column matches; below that the call returns unknown_platform and you must pin the platform manually.
dry_runno1 or true → parse + count + sample preview, no DB writes. Default off (commits).
filenoOriginal filename, written to the audit row in dbo.TicketImportRun. Cosmetic; useful for the dashboard's "recent imports" list.

Body: raw CSV (UTF-8 recommended). Cap: 16 MB. Returns {dry_run, run_id, platform, total_rows, imported_rows, skipped_rows, failed_rows, error_sample}; dry-runs additionally return sample_preview — an array of up to 10 mapped rows with {subject, status, priority, requester_name, requester_email, external_id, tags, would_skip_duplicate, description_preview}. Skipped rows are duplicates against previous imports; failed rows include their original CSV row index in error_sample for triage. Requires write scope.

POST apply_ticket_tags_bulk

/v1/api.ashx?resource=apply_ticket_tags_bulk

Bulk-attach (ticket_id, tag) rows from a CSV body. Closes the Zendesk migration corner case where the export emits tags in a separate file (one (ticket, tag) row per pair) rather than inline on the ticket export. tickets_import carries inline tags only; this endpoint reattaches the separate file post-import. Idempotent — re-running the same CSV is safe (rows already present land in already_had rather than failing).

# Step 1: import the tickets (carries inline tags only)
curl -X POST -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: text/csv" --data-binary @tickets.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=tickets_import"

# Step 2: reattach Zendesk's separate ticket_tags.csv
#   ticket_id,tag
#   4711,billing
#   4711,refund
#   4712,priority-customer
curl -X POST -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: text/csv" --data-binary @ticket_tags.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=apply_ticket_tags_bulk"

Header detection: column 0 is treated as ticket_id (internal long) by default. If the header row's first cell is literally ticket_number, column 0 is treated as the per-tenant ticket number and resolved via tickets's GetByNumber path (cached within the run, so a CSV with N rows for the same ticket only does the lookup once).

Body: raw CSV. Cap: 16 MB. Returns {total_rows, applied, already_had, ticket_missing, failed, error_sample}. Tags can't contain commas (the CSV would break) — emit one row per tag instead. Tag value validation matches tickets_bulk?action=add_tag (length cap 60, lowercased server-side). Requires write scope.

POST DELETE portal_token / portal_issue

/v1/api.ashx?resource=portal_token

Mint or revoke a customer-facing magic-link token that lets an end user (the ticket requester) view their own ticket history without a password. Identity model: token → email; the portal landing page joins the token's email to dbo.Ticket.RequesterEmail and shows every ticket the requester authored. Block 244 ships the auth surface; the actual portal landing page lands in the next iteration (Block 245).

Issuance is idempotent on (site, email): by default, calling POST a second time for the same email returns the existing un-expired un-revoked token rather than creating a new one (preserves bookmark URLs). Pass "reuse_active": false to force-mint a fresh token (e.g. after a security incident).

# POST — mint (or reuse) a token for a requester
curl -X POST -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"requester_email":"[email protected]","expiry_days":30}' \
  https://www.mylivechat.com/v1/api.ashx?resource=portal_token

# Response: returns the FULL token + a canonical URL
#   { "id":42, "requester_email":"[email protected]",
#     "token":"r9aYE...", "token_url":"https://.../portal?token=r9aYE...",
#     "expires_utc":"2026-06-04T..." }

# DELETE — revoke a token (sets RevokedUtc; portal page rejects)
curl -X DELETE -H "Authorization: Bearer mlc_xxx" \
  "https://www.mylivechat.com/v1/api.ashx?resource=portal_token&id=42"

Each successful issue fires the ticket.portal_token_issued webhook (payload INCLUDES the raw token for email-automation tools — treat the payload as a secret, sign + verify the standard way). Default expiry is 30 days; clamp range is 1–365 days. The list endpoint resource=portal_tokens returns metadata only — tokens themselves never come back through GET. Recovery from a lost token requires re-issuing.

Cross-tenant safety: validation reads the token from dbo.PortalToken and returns the stored SiteId; the portal page MUST scope downstream queries to that SiteId rather than trusting any caller-supplied tenant identifier (the page is unauthenticated by design). Requires write scope for issue/revoke; read for list.

Portal landing pages (Block 245): the token_url returned by POST resolves to /portal/index.aspx?token=... — an unauthenticated page that lists every ticket on the requester's email address, with each row linking to /portal/ticket.aspx?token=...&id=N for the comment thread (filtered to non-internal). Cross-requester safety: ticket pages enforce that ticket.RequesterEmail matches the token's email so a guessed ticket id from another customer on the same tenant returns "not yours" rather than leaking. Search engines are blocked via noindex,nofollow,noarchive on every portal page.

Portal reply (Block 246): ticket pages render a compose textarea (8,000-char cap) on non-closed tickets. Submissions POST to /portal/reply.ashx which re-validates the token + the cross-requester check, then calls the same TicketRepository.AddComment path the dashboard uses with AuthorType="requester" and IsInternal=false. The existing ticket.requester_replied webhook + analytics fire as if the reply landed via the agent UI, so existing notification rules (Slack pages, assignee notifications) work unchanged. Closed tickets reject reply submissions; the page hides the composer and shows a "contact support" notice instead so the user isn't misled into typing into a dead form.

Self-service login (Block 263): tenants embed /portal/login.aspx?site=N on their support page. Requesters who lost their email enter the address; the page mints a fresh portal token (idempotent — reuses an active one if present) and fires ticket.portal_token_issued for the email-automation tool to deliver. Anti-enumeration posture: the page renders the same "if we found a record, we sent a link" response regardless of whether the email actually matched a known requester (Stripe / GitHub forgot-password convention). Junk-email protection: the page only mints when the email matches at least one ticket on the tenant — random unknown addresses don't accumulate token rows.

POST kb_import

/v1/api.ashx?resource=kb_import

Bulk-import knowledge-base articles from a Zendesk Guide or Freshdesk Solutions CSV export — or any CSV with title + body columns via the generic preset. Same shape as tickets_import: raw CSV body, idempotent on (site_id, platform, external_id), dry-run returns sample rows. Imported articles land as drafts regardless of source-system status, so admins can review before publishing.

# Dry-run a Zendesk Guide export
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: text/csv" \
  --data-binary @zendesk-articles.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=kb_import&dry_run=1"

# Commit a generic two-column CSV (title,body header)
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: text/csv" \
  --data-binary @custom-kb.csv \
  "https://www.mylivechat.com/v1/api.ashx?resource=kb_import&platform=generic"

Query parameters:

ParamRequiredNotes
platformnoOne of zendesk_guide, freshdesk_solutions, generic. Default auto — auto-detect excludes generic (it would always tie low); pick generic explicitly if your CSV doesn't match either preset.
dry_runno1 or true → preview only.
filenoOriginal filename, recorded in dbo.KbImportRun.

Body: raw CSV. Cap: 16 MB. Returns {dry_run, run_id, platform, total_rows, imported_rows, skipped_rows, failed_rows, error_sample}; dry-runs include sample_preview with {title, category, status, external_id, would_skip_duplicate, body_preview}. Body preview strips HTML tags for display only — the persisted article body is unmodified. Each successful insert fires an article.created webhook, so search-index sync subscribers see imports the same way they see organic creates. Requires write scope.

POST macros_import

/v1/api.ashx?resource=macros_import

Bulk-import canned-reply macros from a Zendesk Macros export, a Freshdesk Canned Responses export, or any CSV with title + body columns via the generic preset. Same shape as tickets_import and kb_import: raw CSV body, idempotent on (site_id, platform, external_id), dry-run returns sample rows. All imported macros land as shared (visible to every agent) since CSV exports rarely encode per-agent ownership and the migration intent is to populate the tenant-wide reply library.

# Dry-run a Zendesk Macros export
curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: text/csv" \
     --data-binary @zendesk-macros.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=macros_import&dry_run=1"

# Commit a Freshdesk export
curl -X POST -H "Authorization: Bearer mlc_xxx" \
     -H "Content-Type: text/csv" \
     --data-binary @freshdesk-canned.csv \
     "https://www.mylivechat.com/v1/api.ashx?resource=macros_import&platform=freshdesk_canned"

Query parameters:

ParamRequiredNotes
platformnoOne of zendesk_macros, freshdesk_canned, generic. Default auto — auto-detect excludes generic; pick generic explicitly if your CSV doesn't match either preset.
dry_runno1 or true → preview only, no DB writes.
filenoOriginal filename, recorded in dbo.MacroImportRun.

Body: raw CSV. Cap: 16 MB. Returns {dry_run, run_id, platform, total_rows, imported_rows, skipped_rows, failed_rows, error_sample}; dry-runs include sample_preview with {title, tags, external_id, would_skip_duplicate, body_preview}. Rows missing either Title or Body land in skipped_rows with the reason in error_sample. Requires write scope.

POST time_log

/v1/api.ashx?resource=time_log

Append a time-tracking entry against a ticket. The most-common integration is a Toggl/Harvest/Clockify webhook firing here when a timer stops on a ticket-tagged entry. Append-only by design — corrections post a negative-minute entry rather than editing, so the audit trail stays intact. The matching read endpoint is /v1/time_log?ticket_id=N.

# Log 25 minutes against ticket 4711
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_id": 4711,
       "minutes":   25,
       "agent_id":  "agent42",
       "agent_name":"Pat Q.",
       "note":      "Repro on staging + drafted reply."}' \
  https://www.mylivechat.com/v1/api.ashx?resource=time_log

# Correction — subtract 10 minutes from a previous entry
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_number": 1042,
       "minutes":      -10,
       "note":         "correction: previous entry double-counted handoff"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=time_log

Body fields: ticket_id or ticket_number (required), minutes (required, non-zero, |m| ≤ 30 days = 43200), agent_id / agent_name (optional, default to api:<token-prefix>), note (optional, ≤500 chars).

POST DELETE ticket_snooze

/v1/api.ashx?resource=ticket_snooze

Suppress a ticket from active queues until a future time, after which it lazily un-snoozes on next read. Routes through the same path as the dashboard snooze button — activity row written, ticket.snoozed outbound webhook fires.

# Snooze until a specific UTC timestamp
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_id": 4711,
       "snoozed_until_utc": "2026-05-01T09:00:00Z"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket_snooze

# Snooze for 4 hours (relative shorthand)
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_id": 4711, "snooze_until_relative": "4h"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket_snooze

# Snooze for an explicit number of minutes
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"ticket_id": 4711, "snooze_minutes": 90}' \
  https://www.mylivechat.com/v1/api.ashx?resource=ticket_snooze

# Manual unsnooze — return the ticket to active queue immediately
curl -X DELETE \
  -H "Authorization: Bearer mlc_xxx" \
  "https://www.mylivechat.com/v1/api.ashx?resource=ticket_snooze&ticket_id=4711"

POST body accepts ANY of three time-form inputs (priority order, first match wins): snoozed_until_utc (ISO 8601 string, alias snooze_until_utc), snooze_minutes (int, max 1 year), or snooze_until_relative (string like "2h" / "1d" / "1w"; max 8760h / 365d / 52w). At least 1 minute in the future is required — closer values return 400 bad_field. Identifies the ticket via ticket_id or ticket_number.

DELETE is idempotent — unsnoozing an already-active ticket returns 200 with was_snoozed: false.

POST articles

/v1/api.ashx?resource=articles

Create a KB article. Default status=draft so a misfired automation never accidentally publishes unfinished content. Pass status=visible in the same body to publish-on-create. Two-phase under the hood (create skeleton + immediate update) but presents as a single atomic POST to the caller.

# Minimal: title only — produces a draft skeleton
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"title": "How to reset your password"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=articles
# Full: publish-on-create with body
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"title":   "How to reset your password",
       "category":"Account",
       "slug":    "reset-password",
       "status":  "visible",
       "body":    "## Steps\n1. Click *Forgot password*..."}' \
  https://www.mylivechat.com/v1/api.ashx?resource=articles

Body: title (required, ≤300 chars); optional category (≤80), slug (≤160), body, excerpt (≤500), status (draft|visible|hidden, default draft), use_for_ai (bool, default true), sort_order (int). Returns the freshly-created article including body.

PATCH article

/v1/api.ashx?resource=article

Partial update. Read-modify-write: only fields explicitly present in the body are touched, everything else is preserved. Common flow is "publish a draft" via {id, status: "visible"} after content is finalized.

# Publish
curl -X PATCH \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"id": 42, "status": "visible"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=article

Updatable fields: title, category, slug, body, excerpt, status, use_for_ai, sort_order. Body must include id (or ?id= in the query string) plus at least one updatable field.

POST PATCH DELETE macros

/v1/api.ashx?resource=macros

Full CRUD on canned-reply macros. API tokens have no agent identity, so every macro created via the public API is shared across the tenant; personal (per-agent) macros are 404'd from the API surface. Author attribution is recorded as api:<token-prefix>.

# Create
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"title": "Refund acknowledgement",
       "body_markdown": "Hi {{requester}},\n\nWe received your refund request...",
       "tags": "refund,billing"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=macros

# Update (read-modify-write — only present fields change)
curl -X PATCH \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"id": 17, "tags": "refund,billing,priority"}' \
  https://www.mylivechat.com/v1/api.ashx?resource=macro

# Delete (hard-delete; use-counts are lost)
curl -X DELETE \
  -H "Authorization: Bearer mlc_xxx" \
  "https://www.mylivechat.com/v1/api.ashx?resource=macro&id=17"

POST body: title (required, ≤200 chars), body_markdown alias body (required, ≤32 KB), tags (optional, ≤400 chars). PATCH accepts the same set; at least one updatable field is required. Substitution variables ({{ticket}}, {{requester}}, etc.) are stored raw — the dashboard renderer expands them at insertion time.

POST PATCH DELETE saved_views

/v1/api.ashx?resource=saved_views

Full CRUD on shared ticket saved-views. Same shared-only discipline as macros: API tokens have no agent identity, so personal views are 404'd from this surface. query_string is the raw URL fragment that reproduces the filter (e.g. status=pending&assignee=unassigned) — opaque text by design so adding new ticket-list filters never requires a saved-view migration.

# Create — defaults is_pinned=true so the chip lands on the tickets page
curl -X POST \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"title": "Urgent & unassigned",
       "query_string": "priority=urgent&assignee=unassigned",
       "is_pinned": true}' \
  https://www.mylivechat.com/v1/api.ashx?resource=saved_views

# Update
curl -X PATCH \
  -H "Authorization: Bearer mlc_xxx" \
  -H "Content-Type: application/json" \
  -d '{"id": 9, "is_pinned": false}' \
  https://www.mylivechat.com/v1/api.ashx?resource=saved_view

# Delete
curl -X DELETE \
  -H "Authorization: Bearer mlc_xxx" \
  "https://www.mylivechat.com/v1/api.ashx?resource=saved_view&id=9"

POST body: title (required, ≤120 chars), query_string (required, may be empty, ≤800 chars), is_pinned (optional, default true). PATCH accepts the same set; at least one updatable field is required.

POST PATCH DELETE tags

/v1/api.ashx?resource=tags

Full CRUD on conversation tags. POST is idempotent on (site_id, name): re-creating an existing tag returns the existing row with created:false. PATCH partial-updates only fields present in the body. DELETE soft-deletes (sets IsEnabled=0) so historical applications keep referring to a valid row.

# POST
curl -X POST -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"name":"vip","color":"#7c3aed","description":"Priority customer"}' \
     https://www.mylivechat.com/v1/api.ashx?resource=tags

# PATCH (rename + recolor in one call)
curl -X PATCH -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"id":42,"name":"high-value","color":"#16a34a"}' \
     https://www.mylivechat.com/v1/api.ashx?resource=tags

# DELETE (soft)
curl -X DELETE -H "Authorization: Bearer TOKEN" \
     "https://www.mylivechat.com/v1/api.ashx?resource=tags&id=42"

POST DELETE apply_tag

/v1/api.ashx?resource=apply_tag

Attach or detach a tag from a chat session. POST is idempotent on (site_id, session_id, tag_id); DELETE returns removed:0 if no application existed. Cross-site tag IDs return 404 not_found.

curl -X POST -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"session_id":"abc123","tag_id":42}' \
     https://www.mylivechat.com/v1/api.ashx?resource=apply_tag

POST PATCH DELETE webhook_subscriptions

/v1/api.ashx?resource=webhook_subscriptions

POST creates a subscription and returns the FULL secret once — store it now to verify HMAC at the receiver.

curl -X POST -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"label":"zapier-prod","url":"https://hooks.zapier.com/...","events":"handoff,feedback"}' \
     https://www.mylivechat.com/v1/api.ashx?resource=webhook_subscriptions

Each delivery POSTs JSON with two headers:

X-MLC-Signature: sha256=<hex_hmac_of_body_using_subscription_secret>
X-MLC-Signature-Version: v1
X-MLC-Timestamp: 2026-04-25T10:00:00Z

X-MLC-Signature-Version identifies the signing scheme. v1 is raw HMAC-SHA256 of the body, hex-encoded, prefixed sha256=. Future scheme changes (e.g. timestamp-prefixed canonical forms) bump this value so existing receivers can opt in safely.

Block 234: canonical timing-safe verification snippets in four languages. Every receiver should use the language's constant-time comparison primitive — a naive === / == comparison leaks timing information to a network attacker who can probe many guesses.

Node.js (use timingSafeEqual; raw body is required — if your framework parsed JSON before you got the body, capture the raw buffer in middleware first):

const crypto = require('crypto');
function verify(req, secret) {
  const sig = req.headers['x-mlc-signature'] || '';
  const mac = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(req.rawBody).digest('hex');
  if (sig.length !== mac.length) return false;
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mac));
}

Python (Flask example; use hmac.compare_digest):

import hmac, hashlib
def verify(request, secret):
    sig = request.headers.get('X-MLC-Signature', '')
    mac = 'sha256=' + hmac.new(
        secret.encode(), request.get_data(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(sig, mac)

PHP (use hash_equals; file_get_contents('php://input') for the raw body):

function verify($secret) {
    $sig = $_SERVER['HTTP_X_MLC_SIGNATURE'] ?? '';
    $body = file_get_contents('php://input');
    $mac = 'sha256=' . hash_hmac('sha256', $body, $secret);
    return hash_equals($sig, $mac);
}

Go (use hmac.Equal; remember to call io.ReadAll(r.Body) before handlers downstream consume it):

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "net/http"
)

func verify(r *http.Request, body []byte, secret string) bool {
    sig := r.Header.Get("X-MLC-Signature")
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(sig), []byte(expected))
}

Common mistakes to avoid:

  • Using parsed JSON instead of raw body bytes. Frameworks that parse JSON in middleware (Express body-parser, Flask request.json, ASP.NET model binding) re-serialize when you read the body, and re-serialization is not byte-equal to what we signed. Always read the raw bytes.
  • String equality on the signature. === / == short-circuit on the first differing byte and leak timing. Use the language's HMAC compare primitive.
  • Trusting X-MLC-Timestamp for replay protection without enforcing a window. The header is informational. If you need replay protection, reject deliveries whose timestamp is more than ~5 minutes outside your server clock; the dispatcher's retry window is shorter than that.

POST rotate_webhook_secret

/v1/api.ashx?resource=rotate_webhook_secret

Generates a fresh 24-byte base64url secret and returns it once. The old secret stops verifying immediately — receivers must update before the next event.

Caveat: any in-flight retry attempts already queued were signed with the old secret and will fail verification at the receiver. This is correct security behavior — rotated secrets MUST invalidate older signatures.

POST test_webhook

/v1/api.ashx?resource=test_webhook

Synthesizes an event:"test" payload, signs it with the subscription's current secret, and POSTs synchronously. Returns the receiver's HTTP status so you can debug HMAC verification end-to-end.

curl -X POST -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" -d '{"id":42}' \
     https://www.mylivechat.com/v1/api.ashx?resource=test_webhook

# 200 with
{ "ok": true, "data": { "id": 42, "delivered": true, "receiver_status": 200, "signature_header": "X-MLC-Signature" } }

PATCH slack_settings / discord_settings / teams_settings

/v1/api.ashx?resource=slack_settings

Update the webhook URL and event filter for any of the three chat-platform notifiers. Each platform validates the URL prefix:

PlatformRequired URL prefix
Slackhttps://hooks.slack.com/
Discordhttps://discord.com/api/webhooks/ or https://discordapp.com/api/webhooks/
TeamsMicrosoft hosts: *.webhook.office.com or *.logic.azure.com
curl -X PATCH -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"webhook_url":"https://hooks.slack.com/services/T/B/X","events":["handoff","feedback"]}' \
     https://www.mylivechat.com/v1/api.ashx?resource=slack_settings
events vocabulary: currently handoff and feedback. Sending an unknown event name returns 400 bad_field.

POST visitors / visitor_event

/v1/api.ashx?resource=visitors

Programmatically upsert a visitor (same data shape as the JS Visitor SDK's identify() call). For event tracking use visitor_event.

curl -X POST -H "Authorization: Bearer TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"visitor_key":"u-12345","email":"[email protected]","name":"Jane","plan":"pro"}' \
     https://www.mylivechat.com/v1/api.ashx?resource=visitors