Skip to main content

Documentation Index

Fetch the complete documentation index at: https://www.quo.com/docs/llms.txt

Use this file to discover all available pages before exploring further.

This is the beta webhook API, available in open beta. For the existing webhook system, see Legacy webhooks.
Every webhook delivery uses the common envelope with the same id, apiVersion, createdAt, type, and data fields. The sections below document only the per-event data wrapper - the part that varies by event type. The type field on the envelope determines which schema below applies.

Quick index

Event typeWhen it fires
message.receivedAn inbound SMS, MMS, or message was received by a Quo number.
message.deliveredAn outbound message was delivered. Not a read receipt.
call.summary.completedA call summary finished generating. May arrive long after the call ends.
call.transcript.completedA call transcript finished processing. Order vs. summary is not guaranteed.
contact.updatedA contact was created or its fields changed.
contact.deletedA contact was deleted. Resource shape matches contact.updated.
To see any event payload against your endpoint without waiting for live traffic, call POST /webhooks/:id/events/test with the eventType you want to inspect.

Field semantics

Two patterns appear in multiple events. Both indicate why a field may be empty rather than implying “no data exists”.

contacts.lookupStatus

Explains why contacts.ids may be empty:
StateMeaning
matchedQuo found one or more matching contacts. ids is populated.
noneQuo checked for matching contacts and found none. ids is [].
unavailableQuo could not determine contact matches because the event lacked sufficient context. Treat ids as unknown, not empty.

participants.resolution

Explains whether participant context was resolved on call events:
StateMeaning
availableWorkspace and external participants are populated correctly.
unavailableParticipant context could not be resolved. Treat empty arrays as unknown rather than as no participants.

Common type aliases

Several events reuse these types. Each event references them by name rather than redefining them inline.
type MessageStatus =
  | 'queued' | 'sending' | 'sent' | 'delivered' | 'undelivered' | 'failed'
  | 'receiving' | 'received' | 'accepted' | 'scheduled' | 'read'
  | 'partially_delivered' | 'canceled'

interface MessageContext {
  phoneNumberId: string | null
  conversationId: string | null
  userId: string
  contacts: { ids: string[]; lookupStatus: 'matched' | 'none' | 'unavailable' }
  senderIdentifier: string
  recipientIdentifiers: string[]
}

interface CallContext {
  phoneNumberId: string | null
  conversationId: string | null
  phoneNumberType: 'shared' | 'private' | 'external' | null
  userId: string
  contacts: { ids: string[]; lookupStatus: 'matched' | 'none' | 'unavailable' }
  participants: {
    workspace: string[]
    external: string[]
    resolution: 'available' | 'unavailable'
  }
}

interface ContactContext {
  userId: string
  sharedWithIds: string[]
}

message.received

An inbound message was received by Quo. Use this as your inbound trigger.
interface MessageReceivedEvent {
  type: 'message.received'
  data: {
    resource: {
      id: string
      direction: 'incoming'
      text: string
      status: MessageStatus
      createdAt: string
    }
    context: MessageContext
    links: { quo: string | null }
  }
}
{
  "data": {
    "resource": {
      "id": "AC-message",
      "direction": "incoming",
      "text": "hello",
      "status": "received",
      "createdAt": "2026-04-13T12:00:00.000Z"
    },
    "context": {
      "phoneNumberId": "PN123",
      "conversationId": "CN123",
      "userId": "US123",
      "contacts": { "ids": ["CT123"], "lookupStatus": "matched" },
      "senderIdentifier": "+15550001111",
      "recipientIdentifiers": ["+15550002222"]
    },
    "links": { "quo": "https://my.quo.com/inbox/..." }
  }
}
senderIdentifier and recipientIdentifiers are raw participant identifiers. They are usually E.164 phone numbers, but direct-number and internal flows can emit non-phone identifiers.

message.delivered

An outbound message was delivered. This is delivery confirmation, not a read receipt.
interface MessageDeliveredEvent {
  type: 'message.delivered'
  data: {
    resource: {
      id: string
      direction: 'outgoing'
      text: string
      status: MessageStatus
      createdAt: string
    }
    context: MessageContext
    links: { quo: string | null }
  }
}
{
  "data": {
    "resource": {
      "id": "AC-message",
      "direction": "outgoing",
      "text": "hello",
      "status": "delivered",
      "createdAt": "2026-04-13T12:00:00.000Z"
    },
    "context": {
      "phoneNumberId": "PN123",
      "conversationId": "CN123",
      "userId": "US123",
      "contacts": { "ids": ["CT123"], "lookupStatus": "matched" },
      "senderIdentifier": "+15550002222",
      "recipientIdentifiers": ["+15550001111"]
    },
    "links": { "quo": "https://my.quo.com/inbox/..." }
  }
}
If you correlate deliveries with conversations, key on context.conversationId and context.phoneNumberId rather than links.quo.

call.summary.completed

A call summary finished processing. This is a summary readiness event, not a call-ended event — the call may have ended much earlier. Trust processingStatus, not arrival time, when interpreting summary state.
interface CallSummaryCompletedEvent {
  type: 'call.summary.completed'
  data: {
    resource: {
      callId: string
      processingStatus: 'absent' | 'in-progress' | 'completed' | 'failed'
      summary: string[] | null
      nextSteps: string[] | null
      fromPhoneNumber: string | null
      handledByAiAgent: boolean
      answeredByUserId: string | null
      jobs: AgentCallSummaryJob[]
    }
    context: CallContext
    links: { quo: string | null }
  }
}

interface AgentCallSummaryJob {
  icon: string
  name: string
  result: { data: Array<{ name: string; value: string | number | boolean }> }
}
{
  "data": {
    "resource": {
      "callId": "AC-summary",
      "processingStatus": "completed",
      "summary": ["Customer asked for pricing details."],
      "nextSteps": ["Send follow-up email."],
      "fromPhoneNumber": null,
      "handledByAiAgent": false,
      "answeredByUserId": null,
      "jobs": []
    },
    "context": {
      "phoneNumberId": "PN123",
      "conversationId": "CN123",
      "phoneNumberType": "shared",
      "userId": "US123",
      "contacts": { "ids": ["CT123"], "lookupStatus": "matched" },
      "participants": {
        "workspace": ["+15550000001"],
        "external": ["+15550000002"],
        "resolution": "available"
      }
    },
    "links": { "quo": "https://my.quo.com/inbox/..." }
  }
}
Notes:
  • summary and nextSteps are arrays when processingStatus === 'completed'; otherwise null.
  • jobs is an empty array when no AI agent job metadata is available.
  • Use callId to correlate the summary with its source call and any transcript event.

call.transcript.completed

A call transcript finished processing. Transcript and summary events are independent and may arrive in either order for the same call.
interface CallTranscriptCompletedEvent {
  type: 'call.transcript.completed'
  data: {
    resource: {
      callId: string
      createdAt: string
      duration: number
      processingStatus: 'absent' | 'in-progress' | 'completed' | 'failed'
      dialogue: DialogueEntry[] | null
    }
    context: CallContext
    links: { quo: string | null }
  }
}

interface DialogueEntry {
  userId: string | null
  identifier: string | null
  content: string
  start: number
  end: number
}
{
  "data": {
    "resource": {
      "callId": "AC-transcript",
      "createdAt": "2026-04-13T12:00:01.000Z",
      "duration": 42,
      "processingStatus": "completed",
      "dialogue": [
        { "userId": "US123", "identifier": null, "content": "Thanks for calling, how can I help?", "start": 0, "end": 3 },
        { "userId": null, "identifier": "+15550000002", "content": "Hi, I wanted to ask about pricing.", "start": 3, "end": 7 }
      ]
    },
    "context": {
      "phoneNumberId": "PN123",
      "conversationId": "CN123",
      "phoneNumberType": "shared",
      "userId": "US123",
      "contacts": { "ids": ["CT123"], "lookupStatus": "matched" },
      "participants": {
        "workspace": ["+15550000001"],
        "external": ["+15550000002"],
        "resolution": "available"
      }
    },
    "links": { "quo": "https://my.quo.com/inbox/..." }
  }
}
Not every dialogue line maps to a Quo user. External participants surface as identifier without a userId; internal speakers surface as userId with or without an identifier.

contact.updated

A contact was created or its fields changed. Use updatedAt for ordering and freshness checks. Contact events are workspace-wide; see Subscription rules.
interface ContactUpdatedEvent {
  type: 'contact.updated'
  data: {
    resource: ContactResource
    context: ContactContext
    links: { quo: string | null }
  }
}

interface ContactResource {
  id: string
  firstName: string | null
  lastName: string | null
  company: string | null
  role: string | null
  location: string | null
  source: string | null
  externalId: string | null
  emails: Array<{ value: string; type: 'email' }>
  phoneNumbers: Array<{ value: string; type: 'phone-number' }>
  customFields: CustomField[]
  createdAt: string
  updatedAt: string
}

type CustomField =
  | { name: string; key: string; id?: string; type: 'string' | 'url' | 'address'; value: string | null }
  | { name: string; key: string; id?: string; type: 'number'; value: number | null }
  | { name: string; key: string; id?: string; type: 'boolean'; value: boolean }
  | { name: string; key: string; id?: string; type: 'date'; value: string | null }
  | { name: string; key: string; id?: string; type: 'multi-select'; value: string[] }
{
  "data": {
    "resource": {
      "id": "CT123",
      "firstName": "Jane",
      "lastName": "Doe",
      "company": null,
      "role": null,
      "location": null,
      "source": null,
      "externalId": null,
      "emails": [{ "value": "[email protected]", "type": "email" }],
      "phoneNumbers": [{ "value": "+15551234567", "type": "phone-number" }],
      "customFields": [
        { "name": "Department", "key": "department", "id": "i1", "type": "multi-select", "value": ["sales"] }
      ],
      "createdAt": "2026-01-01T00:00:00.000Z",
      "updatedAt": "2026-04-13T12:00:00.000Z"
    },
    "context": { "userId": "US123", "sharedWithIds": ["US456"] },
    "links": { "quo": "https://my.quo.com/contacts/CT123" }
  }
}
Notes:
  • customFields[].id is omitted when the source item has no id.
  • Invalid number or date custom field values normalize to null.
  • multi-select values are always arrays.

contact.deleted

A contact was deleted. The resource shape matches contact.updated; the event discriminator is type. Soft-deleted email addresses, phone numbers, and custom fields are removed from the payload rather than delivered with a deletion marker.
interface ContactDeletedEvent {
  type: 'contact.deleted'
  data: {
    resource: ContactResource
    context: ContactContext
    links: { quo: string | null }
  }
}
The JSON shape is identical to contact.updated, with type: "contact.deleted" on the envelope.

See also