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 type | When it fires |
|---|
message.received | An inbound SMS, MMS, or message was received by a Quo number. |
message.delivered | An outbound message was delivered. Not a read receipt. |
call.summary.completed | A call summary finished generating. May arrive long after the call ends. |
call.transcript.completed | A call transcript finished processing. Order vs. summary is not guaranteed. |
contact.updated | A contact was created or its fields changed. |
contact.deleted | A 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”.
Explains why contacts.ids may be empty:
| State | Meaning |
|---|
matched | Quo found one or more matching contacts. ids is populated. |
none | Quo checked for matching contacts and found none. ids is []. |
unavailable | Quo 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:
| State | Meaning |
|---|
available | Workspace and external participants are populated correctly. |
unavailable | Participant 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.
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.
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