# Authentication Source: https://www.quo.com/docs/mdx/api-reference/authentication Learn how to gain API access. ## Prerequisites Before you begin using the Quo API, ensure you have: Need an account? Follow our [account creation guide](https://support.openphone.com/hc/en-us/articles/1500009886621-How-to-create-an-OpenPhone-account). Owner or admin privileges in your Quo workspace. **US Messaging Registration Required:** To send text messages to US numbers via the API, you must complete US Carrier Registration. Learn more [here](https://support.openphone.com/hc/en-us/articles/15519949741463-Guide-to-US-carrier-registration-for-OpenPhone-customers). ## API key generation The Quo API uses API keys for secure authentication. Follow these steps to get started: Access your Quo account. Navigate to the "API" tab under workspace settings. Remember, you need workspace owner or admin privileges to access this tab. Click "Generate API key" and provide a descriptive label. Each key provides full API access. Name your API key based on its intended use (e.g., "production-environment" or "testing-integration"). Spaces are not allowed in the API key name. Include your API key in the Authorization header of each request: ` Authorization: YOUR_API_KEY` The Quo API does not use a Bearer token for authentication. ## Security guidelines Your API key carries the same privileges as your Quo account. Treat it with the same level of security as your password. ### Best practices * Keep your API keys confidential * Don’t share your API keys in publicly accessible areas such as GitHub or client-side code * Regularly rotate your API keys to enhance security * If a key is compromised, revoke it immediately and generate a new one ### Revoking access If a key is compromised or no longer needed: 1. Navigate to the "API" tab in Workspace Settings 2. Locate the specific key 3. Click the ellipsis (three dots) icon and select "Delete" to immediately revoke access 4. Generate a new key if needed Deleting an API key only affects the integrations using that specific key. Other keys and integrations will continue to function normally. # Get a call by ID Source: https://www.quo.com/docs/mdx/api-reference/calls/get-a-call-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/calls/{callId} Get a call by its unique identifier. # Get a summary for a call Source: https://www.quo.com/docs/mdx/api-reference/calls/get-a-summary-for-a-call https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/call-summaries/{callId} Retrieve a detailed summary of a specific call identified by its unique call ID. This endpoint supports summaries for both regular calls and calls handled by Sona. Call summaries are only available on business and scale plans. # Get a transcription for a call Source: https://www.quo.com/docs/mdx/api-reference/calls/get-a-transcription-for-a-call https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/call-transcripts/{id} Retrieve a detailed transcript of a specific call identified by its unique call ID. This endpoint supports transcripts for both regular calls and calls handled by Sona. Call transcripts are only available on business and scale plans. # Get a voicemail for a call Source: https://www.quo.com/docs/mdx/api-reference/calls/get-a-voicemail-for-a-call https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/call-voicemails/{callId} Retrieve a voicemail associated with a specific call. Returns null data fields while the voicemail is processing in our system. Returns competed data fields when the voicemail has finished processing. # Get recordings for a call Source: https://www.quo.com/docs/mdx/api-reference/calls/get-recordings-for-a-call https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/call-recordings/{callId} Retrieve a list of recordings associated with a specific call. The results are sorted chronologically, with the oldest recording segment appearing first in the list. # List calls Source: https://www.quo.com/docs/mdx/api-reference/calls/list-calls https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/calls Fetch a paginated list of calls associated with a specific Quo number and another number. # Changelog Source: https://www.quo.com/docs/mdx/api-reference/changelog Stay up to date with the latest improvements to the API. View [main product Changelog.](https://support.quo.com/changelog) ### Added **`conversationId` in message responses.** All messages endpoints now include `conversationId` in the response body. **Group messaging support.** [`GET /v1/messages`](/docs/mdx/api-reference/messages/list-messages) now supports retrieving group conversation messages via the `participants` array. ### Added **Mark a conversation as done.** [`POST /v1/conversations/{conversationId}/mark-as-done`](/docs/mdx/api-reference/conversations/mark-conversation-as-done) removes a conversation from the inbox without sending a message and returns the updated conversation. **Mark a conversation as open.** [`POST /v1/conversations/{conversationId}/mark-as-open`](/docs/mdx/api-reference/conversations/mark-conversation-as-open) moves a conversation back to the inbox without sending a message and returns the updated conversation. ### Added **Group messages.** [`POST /v1/messages`](/docs/mdx/api-reference/messages/send-a-text-message) now accepts up to 10 phone numbers in the `to` array, sending a single group message to all recipients at once. Sending to a single recipient is unchanged. ### Added **Mark a conversation as read.** [`POST /v1/conversations/{conversationId}/mark-as-read`](/docs/mdx/api-reference/conversations/mark-conversation-as-read) clears a conversation's unread indicator without sending a message and returns the updated conversation. ### Added **Tasks API.** A new set of endpoints for managing tasks is now available. | Method | Endpoint | Description | | -------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | | `GET` | [`/v1/tasks`](/docs/mdx/api-reference/tasks/list-tasks) | List tasks with cursor-based pagination. | | `POST` | [`/v1/tasks`](/docs/mdx/api-reference/tasks/create-a-task) | Create a task linked to a phone number, conversation, or activity. | | `GET` | [`/v1/tasks/{taskId}`](/docs/mdx/api-reference/tasks/gets-a-task-by-id) | Get a task by ID. | | `PUT` | [`/v1/tasks/{taskId}`](/docs/mdx/api-reference/tasks/update-a-task) | Update a task's title and description. | | `DELETE` | [`/v1/tasks/{taskId}`](/docs/mdx/api-reference/tasks/delete-a-task-by-id) | Delete a task by ID. | | `POST` | [`/v1/tasks/{taskId}/complete`](/docs/mdx/api-reference/tasks/complete-a-task) | Mark a task as completed. | | `POST` | [`/v1/tasks/{taskId}/reopen`](/docs/mdx/api-reference/tasks/reopen-a-task) | Reopen a completed task. | | `POST` | [`/v1/tasks/{taskId}/assign`](/docs/mdx/api-reference/tasks/assign-a-user-to-a-task) | Assign a user to a task. | | `POST` | [`/v1/tasks/{taskId}/unassign`](/docs/mdx/api-reference/tasks/unassign-a-user-from-a-task) | Remove a user from a task's assignees. | | `POST` | [`/v1/tasks/{taskId}/change-due-date`](/docs/mdx/api-reference/tasks/change-a-tasks-due-date) | Set a task's due date. | | `POST` | [`/v1/tasks/{taskId}/remove-due-date`](/docs/mdx/api-reference/tasks/remove-a-tasks-due-date) | Clear a task's due date. | | `POST` | [`/v1/tasks/{taskId}/link-conversation`](/docs/mdx/api-reference/tasks/link-a-task-to-a-conversation) | Link a task to a conversation. | | `POST` | [`/v1/tasks/{taskId}/unlink-conversation`](/docs/mdx/api-reference/tasks/unlink-a-task-from-a-conversation) | Unlink a conversation from a task. | ### Added **Call lifecycle events.** Five call lifecycle events are now supported by the beta webhook API, completing event parity with the legacy webhook system: | Event type | When it fires | | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | [`call.ringing`](/docs/mdx/beta/webhooks-event-payloads#call-ringing) | A call started ringing (incoming or outgoing). | | [`call.answered`](/docs/mdx/beta/webhooks-event-payloads#call-answered) | A call connected. Also fires when an outgoing call reaches voicemail. | | [`call.forwarded`](/docs/mdx/beta/webhooks-event-payloads#call-forwarded) | An incoming call was forwarded. Includes `forwardedFrom` and `forwardedTo` phone numbers. | | [`call.missed`](/docs/mdx/beta/webhooks-event-payloads#call-missed) | An incoming call ended without being answered. | | [`call.voicemail.completed`](/docs/mdx/beta/webhooks-event-payloads#call-voicemail-completed) | A voicemail finished processing. Correlate with the source call via `resource.callId`. | `call.answered`, `call.forwarded`, `call.missed`, and `call.voicemail.completed` are new event types with no equivalent in the legacy webhook system. The beta webhook API now covers the full event surface of the legacy system, plus `call.answered`, `call.forwarded`, `call.missed`, and `call.voicemail.completed` — four event types with no equivalent in legacy. See the [beta webhook overview](/docs/mdx/beta/webhooks-overview) and [event payload reference](/docs/mdx/beta/webhooks-event-payloads) for details. ### Added **Beta webhook API (open beta).** A new webhook API is available in open beta. See the [overview](/docs/mdx/beta/webhooks-overview) and [quickstart](/docs/mdx/beta/webhooks-quickstart) to get started. **Unified create endpoint.** `POST /webhooks` replaces the four legacy create endpoints (`/v1/webhooks/messages`, `/v1/webhooks/calls`, `/v1/webhooks/call-summaries`, `/v1/webhooks/call-transcripts`). Message, call, and contact event types can be combined in a single subscription. Up to 50 webhooks per workspace. **Supported event types at launch.** | Event type | When it fires | | ------------------------------------------------------------------------------------------ | ------------------------------------------------- | | [`message.received`](/docs/mdx/beta/webhooks-event-payloads#message-received) | An inbound message was received. | | [`message.delivered`](/docs/mdx/beta/webhooks-event-payloads#message-delivered) | An outbound message was delivered. | | [`call.completed`](/docs/mdx/beta/webhooks-event-payloads#call-completed) | A call ended. Includes final status and duration. | | [`call.recording.completed`](/docs/mdx/beta/webhooks-event-payloads#call-recording-completed) | A call recording finished processing. | | [`call.summary.completed`](/docs/mdx/beta/webhooks-event-payloads#call-summary-completed) | An AI call summary finished generating. | | [`call.transcript.completed`](/docs/mdx/beta/webhooks-event-payloads#call-transcript-completed) | A call transcript finished processing. | | [`contact.updated`](/docs/mdx/beta/webhooks-event-payloads#contact-updated) | A contact was created or updated. | | [`contact.deleted`](/docs/mdx/beta/webhooks-event-payloads#contact-deleted) | A contact was deleted. | Call lifecycle events (`call.ringing`, `call.answered`, `call.forwarded`, `call.missed`, `call.voicemail.completed`) were not yet available at launch — they were added on May 26, 2026. **Payload envelope.** All events share a common `data.resource` / `data.context` / `data.links` structure. `data.resource` contains the primary business object; `data.context` contains routing metadata (phone number, conversation, participants, contact lookup). See [Webhook event payloads](/docs/mdx/beta/webhooks-event-payloads). **Payload versioning.** Each subscription pins a payload version at creation via the `x-quo-api-version` header. Existing subscriptions are unaffected by future version changes. See [Versioning policy](/docs/mdx/beta/webhooks-overview#versioning-policy) for the current version. **Signing.** Deliveries use Standard-Webhooks-compatible headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`) and a `whsec_...` base64 secret, compatible with the Svix SDK. This scheme is not interchangeable with the legacy `OpenPhone-Signature` header — update signature verification before routing beta traffic to an existing endpoint. See [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation). **Delivery inspection.** Send a signed test delivery (`POST /webhooks/:id/events/test`), browse delivery history and per-attempt responses (`GET /webhooks/:id/events`, `GET /webhooks/:id/events/:eventId`), and trigger manual retries (`POST /webhooks/:id/events/:eventId/retry`). See [Webhook API reference](/docs/mdx/beta/webhooks-api-reference). **Signing secret rotation.** `POST /webhooks/:id/rotate` issues a new `whsec_...` key for an existing subscription without changing its event subscriptions or `apiVersion`. For a side-by-side comparison with the legacy system and a no-downtime migration walkthrough, see [Migrating from legacy](/docs/mdx/beta/webhooks-differences-from-current). ### Minor Changes * Adds a property `externalId` to the contact model. Adds `externalId` and `source` as optional parameters to the Create Contact (`POST /contacts`) request. * Adds `externalId` and `source` as optional parameters to the Update Contact (`PATCH /contacts/:id`) request. * Added a route to list contacts (`GET /contacts`). ### Patch Changes * Fixed an issue where creating or updating a contact with an invalid custom field would result in 500 error. Sending an invalid custom field will now result in a 400 "Invalid Custom Field Item" error. ### Patch Changes * Fixed an issue where paginated endpoints would return a string token for the next page at the end of paginated results. Now, they will correctly return the next page token as `null`. * Added a callout that the `totalItems` result field for the paginated endpoints is not functioning as expected and is not returning the true total items count. ### Patch Changes * Fixes an issue where phone numbers in various routes were expected to be in E164 format, but the format was not being validated correctly. ## 1.1.0 ### Minor Changes * Adds a property, `restrictions`, to the objects in the response from list phone numbers (`GET /phone-numbers`). The new property contains information about regional restrictions for outbound calling and messaging from a phone number. ### Patch Changes * Fixed an issue with list calls (`GET /calls`) where sending an empty participants param resulted in a 500 response. Sending an empty participants param will now result in a descriptive 400 response. * Fixes an issue where attempting to send a message to an international number would result in a 500 response if international messaging is not enabled in the workspace. With this fix, the 500 error response changed to a 403 with a descriptive message * Fixes a bug where the GET call recordings endpoint sometimes returned an empty array. * Fixes an issue that was preventing some call records from returning successfully from `GET /calls` * Fixes an issue where getting a contact by id would result in a 500 instead of a 404 when contact is not found. Now this will respond in a 404 with a descriptive message. * Fixes an issue where sending a message that contained only whitespace (`' '`, `'\n'`, etc.) resulted in a 500 error response. Now, this will respond with 400 and a validation error message instead. ### Patch Changes * Fixes an issue with List Calls (`GET /calls`) where the user ID applied by default when the user ID parameter was not sent was being set to the workspace owner instead of the phone number owner. ## 1.0.0 ### Major Changes OpenPhone's Public API v1 release 🚀 Changes from the beta version include: * The `since` query parameter on "list calls" and "list messages" has been deprecated. It used to incorrectly behave as a `createdBefore`. Please use `createdAfter` instead, or `createdBefore` to maintain current functionality. * The `phoneNumberId` field for "send text message" has been deprecated. Please use `from` instead. * `/v0` endpoints have been deprecated. Please use `/v1` instead. # Get contact custom fields Source: https://www.quo.com/docs/mdx/api-reference/contact-custom-fields/get-contact-custom-fields https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/contact-custom-fields Custom contact fields enhance your Quo contacts with additional information beyond standard details like name, company, role, emails and phone numbers. These user-defined fields let you capture business-specific data. While you can only create or modify these fields in Quo itself, this endpoint retrieves your existing custom properties. Use this information to accurately map and include important custom data when creating new contacts via the API. # Create a contact Source: https://www.quo.com/docs/mdx/api-reference/contacts/create-a-contact https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/contacts Create a contact for a workspace. # Delete a contact Source: https://www.quo.com/docs/mdx/api-reference/contacts/delete-a-contact https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json delete /v1/contacts/{id} Delete a contact by its unique identifier. # Get a contact by ID Source: https://www.quo.com/docs/mdx/api-reference/contacts/get-a-contact-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/contacts/{id} Retrieve detailed information about a specific contact in your Quo workspace using the contact's unique identifier. # List contacts Source: https://www.quo.com/docs/mdx/api-reference/contacts/list-contacts https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/contacts Retrieve a paginated list of contacts. You can optionally filter the results by providing external IDs and sources. When no external IDs are provided, all contacts for the organization are returned. # Update a contact by ID Source: https://www.quo.com/docs/mdx/api-reference/contacts/update-a-contact-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json patch /v1/contacts/{id} Modify an existing contact in your Quo workspace using the contact's unique identifier. # List Conversations Source: https://www.quo.com/docs/mdx/api-reference/conversations/list-conversations https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/conversations Fetch a paginated list of conversations of Quo conversations. Can be filtered by user and/or phone numbers. Defaults to all conversations in the Quo organization. Results are returned in descending order based on the most recent conversation. # Mark conversation as done Source: https://www.quo.com/docs/mdx/api-reference/conversations/mark-conversation-as-done https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/conversations/{conversationId}/mark-as-done Mark a conversation as done, removing it from the inbox without sending a message. Returns the updated conversation. # Mark conversation as open Source: https://www.quo.com/docs/mdx/api-reference/conversations/mark-conversation-as-open https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/conversations/{conversationId}/mark-as-open Mark a conversation as open, returning it to the inbox without sending a message. Returns the updated conversation. # Mark conversation as read Source: https://www.quo.com/docs/mdx/api-reference/conversations/mark-conversation-as-read https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/conversations/{conversationId}/mark-as-read Mark a conversation as read, clearing its unread indicator without sending a message. Returns the updated conversation. # API response codes Source: https://www.quo.com/docs/mdx/api-reference/error-codes Quo uses standard HTTP response codes to indicate request status. ## Response code categories * 2xx: Success * 4xx: Client-side errors * 5xx: Server-side errors Some 4xx errors include specific error codes for programmatic handling. ## Common response codes | Code | Status | Description | | ----- | -------------------- | ------------------------------------------------------------- | | `200` | OK | Request successful | | `201` | Created | Resource successfully created | | `202` | Accepted | Request accepted for processing | | `204` | No Content | Request successful, no content returned | | `400` | Bad Request | Invalid parameters | | `401` | Unauthorized | Missing or invalid API key | | `403` | Forbidden | Insufficient permissions or an account setting is not enabled | | `404` | Not Found | Resource doesn't exist | | `409` | Conflict | Conflict with another request | | `422` | Unprocessable Entity | Request is well-formed but contains semantic errors | | `429` | Too Many Requests | Rate limit exceeded | | `500` | Server Error | Quo-side issue | For 429 errors, implement exponential backoff in your requests. # Introduction Source: https://www.quo.com/docs/mdx/api-reference/introduction Welcome to the Quo API! ## Overview The Quo API enables developers to integrate powerful communication features directly into their applications and workflows. Built on REST principles, our API provides a reliable and secure way to programmatically manage phone communications. ## Key features Industry-standard REST API design with JSON responses for easy integration API key-based authentication to ensure secure access to your account All API responses are returned in standardized JSON format Comprehensive documentation and [resources](/docs/mdx/guides) to accelerate development ## Next steps Ready to dive deeper? Here are some helpful resources: * [Authentication guide](/docs/mdx/api-reference/authentication) - Learn about securing your API requests * [Send your first message](/docs/mdx/api-reference/send-your-first-message) - Quick start guide for messaging * [Build with AI LLMs](/docs/mdx/guides/building-with-ai-llms) - Learn how to utilize this API alongside AI # Get a message by ID Source: https://www.quo.com/docs/mdx/api-reference/messages/get-a-message-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/messages/{id} Get a message by its unique identifier. # List messages Source: https://www.quo.com/docs/mdx/api-reference/messages/list-messages https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/messages Retrieve a chronological list of messages exchanged between your Quo number and specified participants, with support for filtering and pagination. # Send a text message Source: https://www.quo.com/docs/mdx/api-reference/messages/send-a-text-message https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/messages Send a text message from your Quo number to a recipient. # Partner directory Source: https://www.quo.com/docs/mdx/api-reference/partner-directory Connect with experts to achieve your customer communications goals. Thinking about building with the Quo API but don’t have the time or in-house expertise to get started? Our trusted Quo Experts are here to help. Browse our [directory of partners](https://www.openphone.com/experts) who offer tailored services to help you make the most of your Quo setup. Whether it's integrating with other tools, optimizing your workflows, or getting onboarded smoothly, we've got you covered. Not sure where to start? [Tell us about your project](https://www.openphone.com/experts/matchmaking), and we'll connect you with the right partner. # Get a phone number by ID Source: https://www.quo.com/docs/mdx/api-reference/phone-numbers/get-a-phone-number-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/phone-numbers/{phoneNumberId} Get a phone number by its unique identifier. # List phone numbers Source: https://www.quo.com/docs/mdx/api-reference/phone-numbers/list-phone-numbers https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/phone-numbers Retrieve the list of phone numbers and users associated with your Quo workspace. # Rate limits Source: https://www.quo.com/docs/mdx/api-reference/rate-limits Quo implements rate limiting to ensure API stability and fair usage. Each API key may make up to **10 requests per second.** Exceeding this limit may result in `429` status code errors. Implement request throttling in your application to stay within rate limits and optimize API usage. # Send your first message Source: https://www.quo.com/docs/mdx/api-reference/send-your-first-message This is a step-by-step guide for sending your first text message. ## Ping! Let's get started sending text messages via the Quo API. **Have you completed Carrier Registration?** If you plan to send text messages to US numbers via the API, you will also need to complete US Carrier Registration. Learn more [here](https://support.openphone.com/hc/en-us/articles/15519949741463-Guide-to-US-carrier-registration-for-OpenPhone-customers). ### 1. Get phone numbers (optional) Make a call to the `GET Phone Numbers` endpoint to retrieve `userId` and `from` for the desired number (the phone number from which you'd like to send a text message). This step is optional if you already know the `userId` and `from` for the desired sending number. ``` curl --request GET \ --url https://api.quo.com/v1/phone-numbers \ --header 'Authorization: YOUR_API_KEY' ``` ### 2. Specify user ID (optional) If you'd like to send the text message as a particular Quo member in your workspace, make sure to include this `userId` in your request body. If `userId` is not specified, the sender will default to the phone number owner. ### 3. Send your message You are now ready to send your first text message! Once you send your text message, you will receive a 202 Success Message via the API. Nice! ``` curl --request POST \ --url https://api.quo.com/v1/messages \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "content": "", "from": "", "to": [ "+15555555555" ], "userId": "" }' ``` Be sure to format phone numbers in E.164 format (+1234567890). ### Summary You are now able to send a text message to anyone in US or Canada. By using the API, you are able to programmatically send texts to your customers. # Assign a user to a task Source: https://www.quo.com/docs/mdx/api-reference/tasks/assign-a-user-to-a-task https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks/{taskId}/assign Assigns the provided user to the list of assignees for the task # Change a task's due date Source: https://www.quo.com/docs/mdx/api-reference/tasks/change-a-tasks-due-date https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks/{taskId}/change-due-date Sets the task's due date # Complete a task Source: https://www.quo.com/docs/mdx/api-reference/tasks/complete-a-task https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks/{taskId}/complete Marks the provided task as completed # Create a task Source: https://www.quo.com/docs/mdx/api-reference/tasks/create-a-task https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks Create a task linked to a phone number, conversation, or conversation activity. Provide exactly one of `phoneNumberId`, `conversationId`, or `activityId`. # Delete a task by ID Source: https://www.quo.com/docs/mdx/api-reference/tasks/delete-a-task-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json delete /v1/tasks/{taskId} Deletes the task with the provided task ID # Gets a task by ID Source: https://www.quo.com/docs/mdx/api-reference/tasks/gets-a-task-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/tasks/{taskId} Retrieve a single task by its ID # Link a task to a conversation Source: https://www.quo.com/docs/mdx/api-reference/tasks/link-a-task-to-a-conversation https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks/{taskId}/link-conversation Links the task with the provided task ID to a conversation # List tasks Source: https://www.quo.com/docs/mdx/api-reference/tasks/list-tasks https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/tasks Retrieve a list of tasks # Remove a task's due date Source: https://www.quo.com/docs/mdx/api-reference/tasks/remove-a-tasks-due-date https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks/{taskId}/remove-due-date Clears the task's due date # Reopen a task Source: https://www.quo.com/docs/mdx/api-reference/tasks/reopen-a-task https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks/{taskId}/reopen Reopens the provided task # Unassign a user from a task Source: https://www.quo.com/docs/mdx/api-reference/tasks/unassign-a-user-from-a-task https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks/{taskId}/unassign Removes the provided user from the assignee list of a task # Unlink a task from a conversation Source: https://www.quo.com/docs/mdx/api-reference/tasks/unlink-a-task-from-a-conversation https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/tasks/{taskId}/unlink-conversation Unlinks the provided conversation from the task # Update a task Source: https://www.quo.com/docs/mdx/api-reference/tasks/update-a-task https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json put /v1/tasks/{taskId} Updates the task's title and description # Get a user by ID Source: https://www.quo.com/docs/mdx/api-reference/users/get-a-user-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/users/{userId} Retrieve detailed information about a specific user in your Quo workspace using the user's unique identifier. # List users Source: https://www.quo.com/docs/mdx/api-reference/users/list-users https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/users Retrieve a paginated list of users in your Quo workspace. # Create a new webhook for call summaries Source: https://www.quo.com/docs/mdx/api-reference/webhooks/create-a-new-webhook-for-call-summaries https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/webhooks/call-summaries Creates a new webhook that triggers on events from call summaries. # Create a new webhook for call transcripts Source: https://www.quo.com/docs/mdx/api-reference/webhooks/create-a-new-webhook-for-call-transcripts https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/webhooks/call-transcripts Creates a new webhook that triggers on events from call transcripts. # Create a new webhook for calls Source: https://www.quo.com/docs/mdx/api-reference/webhooks/create-a-new-webhook-for-calls https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/webhooks/calls Creates a new webhook that triggers on events from calls. # Create a new webhook for messages Source: https://www.quo.com/docs/mdx/api-reference/webhooks/create-a-new-webhook-for-messages https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json post /v1/webhooks/messages Creates a new webhook that triggers on events from messages. # Delete a webhook by ID Source: https://www.quo.com/docs/mdx/api-reference/webhooks/delete-a-webhook-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json delete /v1/webhooks/{id} Delete a webhook by its unique identifier. # Get a webhook by ID Source: https://www.quo.com/docs/mdx/api-reference/webhooks/get-a-webhook-by-id https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/webhooks/{id} Get a webhook by its unique identifier. # Lists all webhooks Source: https://www.quo.com/docs/mdx/api-reference/webhooks/lists-all-webhooks https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json get /v1/webhooks List all webhooks for a user. # Webhook API reference Source: https://www.quo.com/docs/mdx/beta/webhooks-api-reference Endpoints for creating, managing, testing, and inspecting beta webhooks. This is the beta webhook API, available in open beta. For the existing webhook system, see [Legacy webhooks](/docs/mdx/guides/webhooks). Use these endpoints to create, manage, test, and inspect webhooks created with the beta API. Endpoints are grouped into CRUD (create/read/update/delete) and Testing & debugging. Webhooks created through these endpoints are managed through these endpoints and do not appear in the Quo app webhook settings during the beta. Every request requires your Public API key and the API version header: ```bash theme={null} Authorization: YOUR_API_KEY x-quo-api-version: 2026-03-30 ``` ## Endpoint index CRUD: | Method | Path | Purpose | | -------- | ---------------------- | -------------------------------------------------------- | | `GET` | `/webhooks` | [List webhooks](#list-webhooks) for the workspace. | | `POST` | `/webhooks` | [Create a webhook](#create-a-webhook). | | `GET` | `/webhooks/:id` | [Get a webhook](#get-a-webhook). | | `PATCH` | `/webhooks/:id` | [Update a webhook](#update-a-webhook). | | `DELETE` | `/webhooks/:id` | Delete a webhook. | | `POST` | `/webhooks/:id/rotate` | [Rotate the signing secret](#rotate-the-signing-secret). | Testing & debugging: | Method | Path | Purpose | | ------ | ------------------------------------- | ------------------------------------------------------------------- | | `POST` | `/webhooks/:id/events/test` | [Send a test event](#send-a-test-event) to the webhook URL. | | `GET` | `/webhooks/:id/events` | [List deliveries](#list-deliveries). | | `GET` | `/webhooks/:id/events/:eventId` | [Get delivery detail](#get-delivery-detail) including all attempts. | | `POST` | `/webhooks/:id/events/:eventId/retry` | [Retry a delivery](#retry-a-delivery). | ## Supported event types | Event type | When it fires | | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`message.received`](/docs/mdx/beta/webhooks-event-payloads#message-received) | An inbound SMS, MMS, or message was received by a Quo number. | | [`message.delivered`](/docs/mdx/beta/webhooks-event-payloads#message-delivered) | An outbound message was delivered. | | [`message.failed`](/docs/mdx/beta/webhooks-event-payloads#message-failed) | An outbound message failed to deliver. | | [`call.ringing`](/docs/mdx/beta/webhooks-event-payloads#call-ringing) | A call started ringing. Fires for incoming and outgoing. | | [`call.answered`](/docs/mdx/beta/webhooks-event-payloads#call-answered) | A call was connected. `answeredByUserId` identifies the Quo-side user associated with the answer when known. Outgoing calls fire this event for voicemail pickup too. | | [`call.completed`](/docs/mdx/beta/webhooks-event-payloads#call-completed) | A call ended. Terminal lifecycle event with final status and duration. | | [`call.forwarded`](/docs/mdx/beta/webhooks-event-payloads#call-forwarded) | An incoming call was forwarded. Includes the forwarding phone numbers. | | [`call.missed`](/docs/mdx/beta/webhooks-event-payloads#call-missed) | An incoming call ended without being answered. | | [`call.recording.completed`](/docs/mdx/beta/webhooks-event-payloads#call-recording-completed) | A call recording finished processing. | | [`call.summary.completed`](/docs/mdx/beta/webhooks-event-payloads#call-summary-completed) | A call summary finished generating. | | [`call.transcript.completed`](/docs/mdx/beta/webhooks-event-payloads#call-transcript-completed) | A call transcript finished processing. | | [`call.voicemail.completed`](/docs/mdx/beta/webhooks-event-payloads#call-voicemail-completed) | A voicemail was left and finished processing. | | [`contact.updated`](/docs/mdx/beta/webhooks-event-payloads#contact-updated) | A contact was created or its fields changed. | | [`contact.deleted`](/docs/mdx/beta/webhooks-event-payloads#contact-deleted) | A contact was deleted. | For payload shapes, see [Webhook event payloads](/docs/mdx/beta/webhooks-event-payloads). ## List webhooks ```bash curl theme={null} curl https://api.quo.com/webhooks \ -H "Authorization: YOUR_API_KEY" \ -H "x-quo-api-version: 2026-03-30" ``` ```ts Node theme={null} const res = await fetch("https://api.quo.com/webhooks", { headers: { "Authorization": process.env.QUO_API_KEY!, "x-quo-api-version": "2026-03-30", }, }) const { data } = await res.json() ``` Returns all webhooks created through the beta webhook endpoints for the workspace. This endpoint is not paginated. ```json theme={null} { "data": [ { "id": "123", "orgId": "OR123", "label": "Production webhook", "status": "enabled", "url": "https://example.com/webhooks/quo", "createdAt": "2026-04-13T12:00:00.000Z", "updatedAt": "2026-04-13T12:00:00.000Z", "events": ["call.summary.completed", "contact.updated"], "resourceIds": ["PNabc123"], "apiVersion": "2026-03-30" } ] } ``` ## Get a webhook ```bash curl theme={null} curl https://api.quo.com/webhooks/123 \ -H "Authorization: YOUR_API_KEY" \ -H "x-quo-api-version: 2026-03-30" ``` ```ts Node theme={null} const res = await fetch(`https://api.quo.com/webhooks/${id}`, { headers: { "Authorization": process.env.QUO_API_KEY!, "x-quo-api-version": "2026-03-30", }, }) const { data } = await res.json() ``` ```json theme={null} { "data": { "id": "123", "orgId": "OR123", "label": "Production webhook", "status": "enabled", "url": "https://example.com/webhooks/quo", "createdAt": "2026-04-13T12:00:00.000Z", "updatedAt": "2026-04-13T12:00:00.000Z", "events": ["call.summary.completed", "contact.updated"], "resourceIds": ["PNabc123"], "apiVersion": "2026-03-30" } } ``` ## Create a webhook ```bash curl theme={null} curl https://api.quo.com/webhooks \ -X POST \ -H "Authorization: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "x-quo-api-version: 2026-03-30" \ -d '{ "url": "https://example.com/webhooks/quo", "events": ["call.completed", "message.received", "contact.updated"], "resourceIds": ["PNabc123"], "label": "Production webhook", "status": "enabled" }' ``` ```ts Node theme={null} const res = await fetch("https://api.quo.com/webhooks", { method: "POST", headers: { "Authorization": process.env.QUO_API_KEY!, "Content-Type": "application/json", "x-quo-api-version": "2026-03-30", }, body: JSON.stringify({ url: "https://example.com/webhooks/quo", events: ["call.completed", "message.received", "contact.updated"], resourceIds: ["PNabc123"], label: "Production webhook", status: "enabled", }), }) const { data } = await res.json() ``` | Field | Type | Required? | Description | | ------------- | ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `url` | string | **Yes** | Public HTTPS endpoint that receives webhook deliveries. | | `events` | string\[] | **Yes** | One or more [supported event types](#supported-event-types). Message, call, and contact events can be mixed in a single subscription. | | `resourceIds` | string\[] | No | Phone number ids for filtering message and call events, or `["*"]` for all. Defaults to `["*"]`. Contact events are always workspace-wide. | | `label` | string | No | Human-readable label. | | `status` | `"enabled" \| "disabled"` | No | Defaults to `enabled`. | The response includes the `whsec_…` signing secret. Save it as an environment variable — see [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation#webhook-key-format). ```json theme={null} { "data": { "id": "123", "orgId": "OR123", "label": "Production webhook", "status": "enabled", "url": "https://example.com/webhooks/quo", "key": "whsec_...", "createdAt": "2026-04-13T12:00:00.000Z", "updatedAt": "2026-04-13T12:00:00.000Z", "events": ["call.completed", "message.received", "contact.updated"], "resourceIds": ["PNabc123"], "apiVersion": "2026-03-30" } } ``` ## Update a webhook ```bash curl theme={null} curl https://api.quo.com/webhooks/123 \ -X PATCH \ -H "Authorization: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "x-quo-api-version: 2026-03-30" \ -d '{ "events": ["call.summary.completed", "contact.updated"], "resourceIds": ["PNabc123"], "label": "Updated webhook" }' ``` ```ts Node theme={null} const res = await fetch(`https://api.quo.com/webhooks/${id}`, { method: "PATCH", headers: { "Authorization": process.env.QUO_API_KEY!, "Content-Type": "application/json", "x-quo-api-version": "2026-03-30", }, body: JSON.stringify({ events: ["call.summary.completed", "contact.updated"], resourceIds: ["PNabc123"], label: "Updated webhook", }), }) const { data } = await res.json() ``` All fields are optional. Provide only the fields you want to change. | Field | Type | Description | | ------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `url` | string | Replaces the webhook URL. | | `events` | string\[] | Replaces the subscribed event types. | | `resourceIds` | string\[] \| null | Replaces the phone number filters for message and call events. Send `null`, `[]`, or `["*"]` to clear filtering. | | `label` | string \| null | Replaces the label. Send `null` to clear it. | | `status` | `"enabled" \| "disabled"` | Enables or disables delivery. | ## Delete a webhook ```bash theme={null} curl https://api.quo.com/webhooks/123 \ -X DELETE \ -H "Authorization: YOUR_API_KEY" \ -H "x-quo-api-version: 2026-03-30" ``` Returns `204 No Content` on success. ## Rotate the signing secret ```bash curl theme={null} curl https://api.quo.com/webhooks/123/rotate \ -X POST \ -H "Authorization: YOUR_API_KEY" \ -H "x-quo-api-version: 2026-03-30" ``` ```ts Node theme={null} const res = await fetch(`https://api.quo.com/webhooks/${id}/rotate`, { method: "POST", headers: { "Authorization": process.env.QUO_API_KEY!, "x-quo-api-version": "2026-03-30", }, }) const { data } = await res.json() // { key: "whsec_..." } ``` ```json theme={null} { "data": { "key": "whsec_..." } } ``` Store the new key and use it for future signature verification. See [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation). ## Send a test event Sends a real, signed delivery to your webhook URL — the same as a production event — and returns the sample payload inline so you can inspect it without waiting for the delivery to land. ```bash curl theme={null} curl https://api.quo.com/webhooks/123/events/test \ -X POST \ -H "Authorization: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "x-quo-api-version: 2026-03-30" \ -d '{ "eventType": "message.received" }' ``` ```ts Node theme={null} const res = await fetch(`https://api.quo.com/webhooks/${id}/events/test`, { method: "POST", headers: { "Authorization": process.env.QUO_API_KEY!, "Content-Type": "application/json", "x-quo-api-version": "2026-03-30", }, body: JSON.stringify({ eventType: "message.received" }), }) const sample = await res.json() ``` `eventType` must be one of the [supported event types](#supported-event-types). The response is the sample payload sent to your webhook URL. Delivery is asynchronous; use [`GET /webhooks/:id/events`](#list-deliveries) to find the delivery id and inspect what your endpoint returned. ```json theme={null} { "id": "EV-test-message-received", "apiVersion": "2026-03-30", "createdAt": "2026-03-30T18:00:00.000Z", "type": "message.received", "data": { "resource": { "id": "ACsampleactivity0000000000000000", "direction": "incoming", "text": "Hello from Quo! This is a sample message.", "status": "received", "createdAt": "2026-03-30T18:00:00.000Z" }, "context": { "phoneNumberId": "PNsamplephonenumber000000000000", "conversationId": "CNsampleconversation000000000000", "userId": "USsampleus", "contacts": { "ids": ["CTsampleContact01234"], "lookupStatus": "matched" }, "senderIdentifier": "+15555551234", "recipientIdentifiers": ["+15555555678"] }, "links": { "quo": "https://my.quo.com/..." } } } ``` ## List deliveries ```bash curl theme={null} curl "https://api.quo.com/webhooks/123/events?limit=10" \ -H "Authorization: YOUR_API_KEY" \ -H "x-quo-api-version: 2026-03-30" ``` ```ts Node theme={null} const params = new URLSearchParams({ limit: "10" }) const res = await fetch(`https://api.quo.com/webhooks/${id}/events?${params}`, { headers: { "Authorization": process.env.QUO_API_KEY!, "x-quo-api-version": "2026-03-30", }, }) const { data, nextCursor } = await res.json() ``` | Parameter | Type | Description | | --------------- | ------------------------------------------------- | ------------------------------------------------- | | `limit` | number | Page size. | | `after` | string | Cursor from the previous response's `nextCursor`. | | `status` | `"success" \| "pending" \| "sending" \| "failed"` | Filter by delivery status. | | `eventTypes` | string\[] | Restrict results to specific event types. | | `createdBefore` | ISO-8601 string | Only include deliveries created before this time. | | `createdAfter` | ISO-8601 string | Only include deliveries created after this time. | Delivery statuses: | Status | Meaning | | --------- | ---------------------------------------------------------------- | | `success` | At least one delivery attempt returned a `2xx` response. | | `pending` | Delivery is queued and has not attempted yet. | | `sending` | Delivery is in progress or waiting for a retry attempt. | | `failed` | All retry attempts have been exhausted without a `2xx` response. | ```json theme={null} { "data": [ { "id": "msg_2abcDEFghiJKLmnoPQRstu", "eventType": "message.received", "status": "success", "nextAttemptAt": null, "createdAt": "2026-04-13T12:00:00.000Z" } ], "nextCursor": "eyJsYXN0SWQiOiJtc2dfMmFiY0RFRmdoaUpLTG1ub1BRUnN0dSJ9" } ``` ## Get delivery detail ```bash curl theme={null} curl https://api.quo.com/webhooks/123/events/msg_2abcDEFghiJKLmnoPQRstu \ -H "Authorization: YOUR_API_KEY" \ -H "x-quo-api-version: 2026-03-30" ``` ```ts Node theme={null} const res = await fetch( `https://api.quo.com/webhooks/${id}/events/${eventId}`, { headers: { "Authorization": process.env.QUO_API_KEY!, "x-quo-api-version": "2026-03-30", }, } ) const { data } = await res.json() ``` Delivery detail includes the request body and all attempts, ordered most-recent first. ```json theme={null} { "data": { "id": "msg_2abcDEFghiJKLmnoPQRstu", "eventType": "message.received", "createdAt": "2026-04-13T12:00:00.000Z", "requestBody": { "id": "EV123", "apiVersion": "2026-03-30", "createdAt": "2026-04-13T12:00:00.000Z", "type": "message.received", "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/..." } } }, "attempts": [ { "id": "atmpt_2abcDEFghiJKLmnoPQRstu", "timestamp": "2026-04-13T12:00:01.000Z", "status": "success", "responseStatusCode": 200, "responseBody": "{\"ok\":true}", "responseDurationMs": 123, "triggerType": "scheduled", "url": "https://example.com/webhooks/quo" } ] } } ``` ## Retry a delivery ```bash curl theme={null} curl https://api.quo.com/webhooks/123/events/msg_2abcDEFghiJKLmnoPQRstu/retry \ -X POST \ -H "Authorization: YOUR_API_KEY" \ -H "x-quo-api-version: 2026-03-30" ``` ```ts Node theme={null} const res = await fetch( `https://api.quo.com/webhooks/${id}/events/${eventId}/retry`, { method: "POST", headers: { "Authorization": process.env.QUO_API_KEY!, "x-quo-api-version": "2026-03-30", }, } ) ``` The retry is queued asynchronously and returns `202 Accepted`. Use [`GET /webhooks/:id/events/:eventId`](#get-delivery-detail) to inspect the new attempt. ## See also * [Quickstart](/docs/mdx/beta/webhooks-quickstart) * [Overview](/docs/mdx/beta/webhooks-overview) * [Webhook event payloads](/docs/mdx/beta/webhooks-event-payloads) * [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation) * [Migrating from legacy](/docs/mdx/beta/webhooks-differences-from-current) # Migrating from legacy Source: https://www.quo.com/docs/mdx/beta/webhooks-differences-from-current What changes between legacy and beta webhooks, and how to roll over without downtime. The beta webhook API is not a drop-in replacement for the legacy webhook system. Management, signing, and payload shapes all change. This page summarizes the differences and walks through a no-downtime migration. Quo currently has separate webhook systems for app-managed webhooks and API-managed webhooks. Webhooks created in the Quo app are managed in the app, and webhooks created through the current API are managed through the API. During open beta, beta webhooks are managed separately through the beta webhook endpoints and do not appear in Quo app settings. We plan to unify API and app webhook management. ## What changes | Area | Legacy | Beta | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Management | App settings (for app-managed webhooks) and the existing API (for API-managed webhooks). | Beta webhooks are created and managed through the beta webhook endpoints. We plan to unify API and app webhook management. | | Create endpoints | Four near-duplicate create endpoints split by event family (`/v1/webhooks/messages`, `/v1/webhooks/calls`, `/v1/webhooks/call-summaries`, `/v1/webhooks/call-transcripts`). | One `POST /webhooks` with an `events` array. Mix message, call, and contact subscriptions in a single webhook. | | Event coverage | Calls, messages, contacts, transcripts. | Messages, call lifecycle events, call recordings, call summaries, call transcripts, voicemails, and contacts. | | Payload shape | `data.object` with object-specific fields. | `data.resource` + `data.context` + `data.links` envelope. | | Filtering | Per-webhook event subscription, per-phone-number for some event types. | `resourceIds` filter for message and call events; contact events workspace-wide. | | Signature header | `OpenPhone-Signature`. | `webhook-id` + `webhook-timestamp` + `webhook-signature` (Standard-Webhooks-compatible). | | Signing secret | OpenPhone-format secret. | `whsec_…` base64 secret, compatible with the Svix SDK. | | Versioning | Single static API version. | Date-versioned via `x-quo-api-version`. Pinned per subscription at creation. | | Delivery inspection | None. | Test events, delivery history, per-attempt detail, manual retry. | **The signature schemes are not interchangeable.** Code that verifies the legacy `OpenPhone-Signature` header will reject every beta delivery. You must update verification before pointing beta traffic at an existing endpoint — see [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation). ## Field remap The biggest payload change is the split between the primary record and surrounding context. | Legacy field | Beta field | | --------------------------- | ---------------------------- | | `data.object.id` | `data.resource.id` | | `data.object.text` | `data.resource.text` | | `data.object.phoneNumberId` | `data.context.phoneNumberId` | | `data.object.userId` | `data.context.userId` | | `data.object.contactIds` | `data.context.contacts.ids` | | `data.deepLink` | `data.links.quo` | ### Side-by-side: `message.received` ```json Before (legacy) theme={null} { "id": "EV0ea54...", "apiVersion": "v4", "type": "message.received", "data": { "object": { "id": "AC123", "text": "hello", "direction": "incoming", "phoneNumberId": "PN123", "userId": "US123", "contactIds": ["CT123"] }, "deepLink": "https://my.quo.com/inbox/..." } } ``` ```json After (beta) theme={null} { "id": "EV-message-received", "apiVersion": "2026-03-30", "type": "message.received", "data": { "resource": { "id": "AC123", "text": "hello", "direction": "incoming", "status": "received", "createdAt": "2026-04-13T12:00:00.000Z" }, "context": { "phoneNumberId": "PN123", "userId": "US123", "contacts": { "ids": ["CT123"], "lookupStatus": "matched" }, "senderIdentifier": "+15550001111", "recipientIdentifiers": ["+15550002222"] }, "links": { "quo": "https://my.quo.com/inbox/..." } } } ``` Three structural changes to notice: 1. Resource identifiers (id, text, status, createdAt) live under `resource`. 2. Routing context (phoneNumberId, userId, contacts) lives under `context`. 3. Deep links live under `links.quo` — and are explicitly nullable. ## Rollout procedure The recommended migration runs both webhook systems side by side, compares deliveries, and cuts over once you've verified parity. Each step lists what's running where. Update your endpoint to accept the new `webhook-id` / `webhook-timestamp` / `webhook-signature` headers and a `whsec_…` secret. See [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation). Your endpoint still verifies legacy `OpenPhone-Signature` deliveries normally — this step adds a second verifier, doesn't replace the first. *Running:* legacy webhooks only. Create a beta webhook with `status: "disabled"` and the same target URL. This pins the subscription to `2026-03-30` and gives you a `whsec_…` secret without firing any traffic yet. *Running:* legacy webhooks only. Add idempotency keyed on the `webhook-id` header for beta delivery retries (see [Idempotency](/docs/mdx/beta/webhooks-overview#idempotency)). If legacy and beta both send the same business event, use business-field correlation to avoid duplicate side effects across systems. *Running:* legacy webhooks only. Flip the beta subscription to `status: "enabled"`. Both systems now deliver events; your handler verifies both signature schemes and dedupes beta retries. *Running:* legacy + beta in parallel. Use [`GET /webhooks/:id/events`](/docs/mdx/beta/webhooks-api-reference#list-deliveries) to inspect beta deliveries and confirm they match what you expected. Common things to verify: `data.context.contacts.ids` matches your stored `contactIds`; `data.links.quo` is non-`null` where you previously used `deepLink`; `senderIdentifier` and `recipientIdentifiers` look right. *Running:* legacy + beta in parallel. Once parity is verified, disable the legacy webhook in the Quo app or via the legacy API. Your endpoint now serves only beta traffic. *Running:* beta only. ### If something goes wrong * **Verification failures on every beta delivery.** Most likely a body re-serialization issue. See [Framework gotchas](/docs/mdx/beta/webhooks-signature-validation#framework-gotchas). * **Duplicate processing observed.** For beta retries, confirm your idempotency store TTL covers the full retry window and that you keyed on the `webhook-id` header. For duplicate processing across legacy and beta, correlate on stable business fields such as message id, call id, contact id, event type, and event timestamp. * **Need to roll back.** Set the beta subscription `status: "disabled"`. Legacy continues firing because it was never disabled until step 6. ## Dual-running both systems While both systems are active, the same business event may arrive through both systems. Two patterns work: * **Single endpoint, single handler.** Your handler verifies whichever signature header is present. Use `webhook-id` to dedupe beta retries, and use business-field correlation if you need to suppress duplicate processing across legacy and beta deliveries. * **Separate endpoints.** Point the beta subscription at a distinct URL like `/webhooks/quo-beta`. This keeps verification logic isolated and makes traffic patterns easy to compare during cutover. In either case, treat any `2xx` response as accepting the delivery. Returning a non-`2xx` from the wrong handler will trigger retries you don't want. ## See also * [Overview](/docs/mdx/beta/webhooks-overview) * [Quickstart](/docs/mdx/beta/webhooks-quickstart) * [Webhook API reference](/docs/mdx/beta/webhooks-api-reference) * [Webhook event payloads](/docs/mdx/beta/webhooks-event-payloads) * [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation) # Webhook event payloads Source: https://www.quo.com/docs/mdx/beta/webhooks-event-payloads Schemas, examples, and field semantics for every supported event type. This is the beta webhook API, available in open beta. For the existing webhook system, see [Legacy webhooks](/docs/mdx/guides/webhooks). Every webhook delivery uses the [common envelope](/docs/mdx/beta/webhooks-overview#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`](#message-received) | An inbound SMS, MMS, or message was received by a Quo number. | | [`message.delivered`](#message-delivered) | An outbound message was delivered. Not a read receipt. | | [`message.failed`](#message-failed) | An outbound message failed to deliver. Includes a carrier or provider error code when available. | | [`call.ringing`](#call-ringing) | A call started ringing. Fires for incoming and outgoing. Context is narrower than other call events. | | [`call.answered`](#call-answered) | A call was connected. `answeredByUserId` identifies the Quo-side user associated with the answer when known. Outgoing calls fire this event for voicemail pickup too. | | [`call.completed`](#call-completed) | A call ended. Terminal lifecycle event with the final `status` and `duration`. | | [`call.forwarded`](#call-forwarded) | An incoming call was forwarded. Includes the forwarding phone numbers. | | [`call.missed`](#call-missed) | An incoming call ended without being answered. Outgoing calls do not fire this event. | | [`call.recording.completed`](#call-recording-completed) | A call recording finished processing. May arrive after `call.completed`. | | [`call.summary.completed`](#call-summary-completed) | A call summary finished generating. May arrive long after the call ends. | | [`call.transcript.completed`](#call-transcript-completed) | A call transcript finished processing. Order vs. summary is not guaranteed. | | [`call.voicemail.completed`](#call-voicemail-completed) | A voicemail was left and finished processing. Use `resource.callId` to correlate with the source call lifecycle. | | [`contact.updated`](#contact-updated) | A contact was created or its fields changed. | | [`contact.deleted`](#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`](/docs/mdx/beta/webhooks-api-reference#send-a-test-event) 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: | 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. ```ts theme={null} 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 CallRingingContext { phoneNumberId: string | null conversationId: string | null userId: string } interface ContactContext { userId: string sharedWithIds: string[] } ``` ## `message.received` An inbound message was received by Quo. Use this as your inbound trigger. ```ts theme={null} interface MessageReceivedEvent { type: 'message.received' data: { resource: { id: string direction: 'incoming' text: string media: Array<{ type?: string | null; url: string }> status: MessageStatus createdAt: string } context: MessageContext links: { quo: string | null } } } ``` ```json theme={null} { "data": { "resource": { "id": "AC-message", "direction": "incoming", "text": "hello", "media": [], "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. ```ts theme={null} interface MessageDeliveredEvent { type: 'message.delivered' data: { resource: { id: string direction: 'outgoing' text: string media: Array<{ type?: string | null; url: string }> status: MessageStatus createdAt: string } context: MessageContext links: { quo: string | null } } } ``` ```json theme={null} { "data": { "resource": { "id": "AC-message", "direction": "outgoing", "text": "hello", "media": [], "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`. ## `message.failed` An outbound message failed to deliver. `resource.errorCode` contains the carrier or provider failure code when one is available; it is `null` when the failure reason is unknown. ```ts theme={null} interface MessageFailedEvent { type: 'message.failed' data: { resource: { id: string direction: 'outgoing' text: string status: MessageStatus errorCode: string | null createdAt: string } context: MessageContext links: { quo: string | null } } } ``` ```json theme={null} { "data": { "resource": { "id": "AC-message", "direction": "outgoing", "text": "hello", "status": "failed", "errorCode": "30006", "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/..." } } } ``` `errorCode` is the raw code returned by the carrier or messaging provider. It is present only on `message.failed` and is absent from all other message events. ## `call.ringing` `call.ringing` fires once at the start of an incoming or outgoing call. It is intentionally lightweight because call context is not fully available yet. ```ts theme={null} interface CallRingingEvent { type: 'call.ringing' data: { resource: { id: string direction: 'incoming' | 'outgoing' createdAt: string updatedAt: string } context: CallRingingContext links: { quo: string | null } } } ``` ```json theme={null} { "data": { "resource": { "id": "AC-call", "direction": "incoming", "createdAt": "2026-04-13T12:00:00.000Z", "updatedAt": "2026-04-13T12:00:00.000Z" }, "context": { "phoneNumberId": "PN123", "conversationId": "CN123", "userId": "US123" }, "links": { "quo": "https://my.quo.com/inbox/..." } } } ``` Later call events include richer context such as `contacts`, `participants`, and `phoneNumberType`. ## `call.answered` A call was connected. `answeredByUserId` is the Quo user associated with the answer when known. It is not the external party who answered an outgoing call. ```ts theme={null} interface CallAnsweredEvent { type: 'call.answered' data: { resource: { id: string direction: 'incoming' | 'outgoing' createdAt: string answeredAt: string | null answeredByUserId: string | null updatedAt: string | null } context: CallContext links: { quo: string | null } } } ``` ```json theme={null} { "data": { "resource": { "id": "AC-call", "direction": "incoming", "createdAt": "2026-04-13T11:59:55.000Z", "answeredAt": "2026-04-13T12:00:00.000Z", "answeredByUserId": "US123", "updatedAt": "2026-04-13T12:00:00.000Z" }, "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/..." } } } ``` `call.answered` also fires on outgoing calls when the recipient's voicemail picks up. Treat this event as "the call connected" rather than "a person is on the line." ## `call.completed` A call ended. This is the terminal lifecycle event with the final `status`, `duration`, and timestamps. Recording, summary, transcript, and voicemail artifacts (if any) arrive as separate events and may be delayed by minutes. ```ts theme={null} interface CallCompletedEvent { type: 'call.completed' data: { resource: { id: string direction: 'incoming' | 'outgoing' status: CallCompletedStatus createdAt: string answeredAt: string | null completedAt: string | null updatedAt: string | null duration: number | null hasVoicemail: boolean } context: CallContext links: { quo: string | null } } } type CallCompletedStatus = | 'answered' | 'unanswered' | 'failed' | 'forwarded' | 'abandoned' | 'ai-handled' | 'unknown' ``` ```json theme={null} { "data": { "resource": { "id": "AC-call", "direction": "incoming", "status": "answered", "createdAt": "2026-04-13T11:59:55.000Z", "answeredAt": "2026-04-13T12:00:00.000Z", "completedAt": "2026-04-13T12:00:55.000Z", "updatedAt": "2026-04-13T12:00:55.000Z", "duration": 55, "hasVoicemail": false }, "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/..." } } } ``` Possible values for `status`: | Value | Meaning | | ------------ | ---------------------------------------------------------------------------------------------------------------------- | | `answered` | The call connected. For outgoing calls, this may be either a person answering or the recipient's voicemail picking up. | | `unanswered` | The call ended without being answered. May still have a voicemail — check `hasVoicemail`. | | `failed` | The call could not be placed or connected. | | `forwarded` | The call was forwarded to another number. | | `abandoned` | The caller hung up before the call was answered. | | `ai-handled` | An AI agent handled the call. | | `unknown` | Quo could not classify the final state. | * `hasVoicemail` indicates whether a voicemail was left. The voicemail itself arrives as a separate [`call.voicemail.completed`](#call-voicemail-completed) event. * `duration` is in seconds and may be `null` for calls that never connected. ## `call.forwarded` An incoming call was forwarded. This event carries the phone numbers involved in the forward. Other call lifecycle events do not include `forwardedFrom` or `forwardedTo`. ```ts theme={null} interface CallForwardedEvent { type: 'call.forwarded' data: { resource: { id: string createdAt: string updatedAt: string | null forwardedFrom: string forwardedTo: string } context: CallContext links: { quo: string | null } } } ``` ```json theme={null} { "data": { "resource": { "id": "AC-call", "createdAt": "2026-04-13T11:59:55.000Z", "updatedAt": "2026-04-13T12:00:00.000Z", "forwardedFrom": "+15550000001", "forwardedTo": "+15550000003" }, "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/..." } } } ``` ## `call.missed` An incoming call ended without being answered. Outgoing calls do not produce this event — they terminate with `call.completed` and a non-`answered` status. The payload is intentionally minimal. To get the full call shape, look up the call by `resource.id` in the API. ```ts theme={null} interface CallMissedEvent { type: 'call.missed' data: { resource: { id: string createdAt: string updatedAt: string } context: CallContext links: { quo: string | null } } } ``` ```json theme={null} { "data": { "resource": { "id": "AC-call", "createdAt": "2026-04-13T11:59:55.000Z", "updatedAt": "2026-04-13T12:00:30.000Z" }, "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/..." } } } ``` ## `call.recording.completed` A call recording finished processing. May arrive after `call.completed`. `recordings` is always an array — an empty `[]` means no recording metadata is available in this payload. ```ts theme={null} interface CallRecordingCompletedEvent { type: 'call.recording.completed' data: { resource: { id: string direction: 'incoming' | 'outgoing' createdAt: string answeredAt: string | null completedAt: string | null updatedAt: string | null duration: number | null recordings: CallRecording[] } context: CallContext links: { quo: string | null } } } interface CallRecording { id: string | null duration: number | null startTime: string | null type: string | null url: string | null } ``` ```json theme={null} { "data": { "resource": { "id": "AC-call", "direction": "incoming", "createdAt": "2026-04-13T11:59:55.000Z", "answeredAt": "2026-04-13T12:00:00.000Z", "completedAt": "2026-04-13T12:00:55.000Z", "updatedAt": "2026-04-13T12:01:05.000Z", "duration": 55, "recordings": [ { "id": "REabc123", "duration": 55, "startTime": "2026-04-13T12:00:00.000Z", "type": "audio/mpeg", "url": "https://recordings.example.com/REabc123.mp3" } ] }, "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/..." } } } ``` Download or persist the recording file rather than relying on the URL for long-term access. ## `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. ```ts theme={null} 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 }> } } ``` ```json theme={null} { "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. ```ts theme={null} 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 } ``` ```json theme={null} { "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`. ## `call.voicemail.completed` A voicemail was left and finished processing. `resource.id` is the voicemail activity id. `resource.callId` is the source call activity id, or `null` if the source call could not be resolved. Use `resource.callId` to correlate the voicemail with the rest of the call lifecycle. ```ts theme={null} interface CallVoicemailCompletedEvent { type: 'call.voicemail.completed' data: { resource: { id: string voicemailId: string | null callId: string | null direction: 'incoming' | 'outgoing' duration: number from: string to: string transcript: string | null recordingUrl: string | null createdAt: string updatedAt: string } context: CallContext links: { quo: string | null } } } ``` ```json theme={null} { "data": { "resource": { "id": "AC-voicemail", "voicemailId": "VM123", "callId": "AC-source-call", "direction": "incoming", "duration": 18, "from": "+15550000002", "to": "+15550000001", "transcript": "Hi, leaving a quick message about the proposal...", "recordingUrl": "https://recordings.example.com/VM123.mp3", "createdAt": "2026-04-13T12:01:00.000Z", "updatedAt": "2026-04-13T12:01:10.000Z" }, "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/..." } } } ``` * `transcript` is `null` while processing or if transcription was not available for the voicemail. * Download or persist the voicemail recording rather than relying on `recordingUrl` for long-term access. * If the source call context cannot be fully resolved, `participants.resolution` may be `unavailable` and participant arrays may be empty. If the source call itself cannot be resolved, `resource.callId` will be `null` and `phoneNumberType` will be `null`. ## `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](/docs/mdx/beta/webhooks-overview#subscription-rules). ```ts theme={null} 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[] } ``` ```json theme={null} { "data": { "resource": { "id": "CT123", "firstName": "Jane", "lastName": "Doe", "company": null, "role": null, "location": null, "source": null, "externalId": null, "emails": [{ "value": "jane@example.com", "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`](#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. ```ts theme={null} 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 * [Overview](/docs/mdx/beta/webhooks-overview) * [Webhook API reference](/docs/mdx/beta/webhooks-api-reference) * [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation) * [Migrating from legacy](/docs/mdx/beta/webhooks-differences-from-current) # Overview Source: https://www.quo.com/docs/mdx/beta/webhooks-overview Concepts, delivery semantics, and versioning for the beta webhook API. This is the beta webhook API, available in open beta. For the existing webhook system, see [Legacy webhooks](/docs/mdx/guides/webhooks). The beta webhook API delivers real-time events to an HTTPS endpoint you control. Use it to: * Update your CRM when an inbound SMS arrives (`message.received`). * Log calls in your system as soon as they end (`call.completed`), with AI summaries and transcripts following (`call.summary.completed`, `call.transcript.completed`). * Sync contact changes between Quo and your system (`contact.updated`). If you want to ship a working integration first and read concepts second, jump to the [Quickstart](/docs/mdx/beta/webhooks-quickstart). You can send a real, signed test delivery without writing any handler code using `POST /webhooks/:id/events/test`. See [Send a test event](/docs/mdx/beta/webhooks-api-reference#send-a-test-event). ## Common envelope Every webhook delivery uses the same top-level shape. The `type` field determines the schema of `data`. ```json theme={null} { "id": "EV123", "apiVersion": "2026-03-30", "createdAt": "2026-04-13T12:00:00.000Z", "type": "call.summary.completed", "data": { "resource": {}, "context": {}, "links": { "quo": "https://my.quo.com/..." } } } ``` | Field | Meaning | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Stable identifier for the event across retries and payload versions. Not a delivery identifier — for delivery-level dedupe, use the `webhook-id` header. | | `apiVersion` | Payload version recorded when the webhook was created. | | `createdAt` | When the underlying event occurred. The send time is in the `webhook-timestamp` header. | | `type` | The event name. Discriminates the schema of `data`. | | `data.resource` | Primary business object for the event. | | `data.context` | Surrounding metadata (phone number, conversation, participants, contact lookup, sharing). | | `data.links.quo` | Quo app deep link, or `null` when none is available. | Treat all event, delivery, and resource ids as opaque strings. ## Versioning policy * **Format.** Versions are date strings (`YYYY-MM-DD`). The current version is `2026-03-30`. * **Stability.** A webhook's version is recorded at creation and never changes for that subscription. Existing deliveries continue using the version they were created with. * **Pinning.** Set `x-quo-api-version` when calling `POST /webhooks`. Future versions may add event types or make breaking payload changes; new deliveries use the older shape until you create a new subscription on the newer version. See [Migrating from legacy](/docs/mdx/beta/webhooks-differences-from-current) for moving existing integrations. ## Subscription rules * `message.*` and `call.*` events accept a `resourceIds` filter (phone number ids, or `["*"]`). * `contact.*` events are workspace-wide. For contact-only webhooks, omit `resourceIds`. * Mixed subscriptions are supported. `resourceIds` filters the activity events above; contact events are always delivered workspace-wide. * A workspace can have at most 50 webhooks created with the beta API. ## Delivery semantics ### Idempotency Quo will retry deliveries on any non-`2xx` response, so your endpoint must tolerate seeing the same delivery more than once. Use the `webhook-id` header as an idempotency key — it is stable across retries and distinct from the envelope `id`. ```ts theme={null} if (await store.has(req.header("webhook-id"))) return res.status(200).end() await store.add(req.header("webhook-id"), { ttlSeconds: 60 * 60 * 28 }) ``` Store processed ids for at least 28 hours - long enough to cover the full retry window. ### Retries Failed deliveries are retried automatically. Any `2xx` response marks a delivery accepted; any non-`2xx` response triggers the next retry. After all retries fail, the delivery is marked failed and not retried further. | Attempt | Delay from previous | Cumulative | | ------- | ------------------- | ---------- | | 1 | Immediate | 0 | | 2 | 5 seconds | 5s | | 3 | 5 minutes | 5m 5s | | 4 | 30 minutes | 35m 5s | | 5 | 2 hours | 2h 35m 5s | | 6 | 5 hours | 7h 35m 5s | | 7 | 10 hours | 17h 35m 5s | | 8 | 10 hours | 27h 35m 5s | If every attempt fails, the delivery is marked failed after roughly 27 hours and 35 minutes from the initial attempt. You can also trigger a manual retry with `POST /webhooks/:id/events/:eventId/retry`. ### Ordering Delivery order is not guaranteed. Events can arrive out of order across event families and, occasionally, within a single resource — for example, a `call.transcript.completed` may arrive before the corresponding `call.summary.completed`. Design handlers to be order-independent: * Compare `data.resource.updatedAt` (or `createdAt` for terminal events) against your stored state and ignore stale events. * Don't drive state machines from arrival order. Treat each event as a freshness check on the resource it references. ```ts theme={null} const incoming = event.data.resource const stored = await db.contacts.get(incoming.id) if (stored && stored.updatedAt >= incoming.updatedAt) return // stale, ignore await db.contacts.upsert(incoming) ``` ## Headers on every delivery | Header | Format | Purpose | | ------------------- | --------------------------------------- | --------------------------------------------------------------- | | `webhook-id` | string | Idempotency key. Stable across retries. | | `webhook-timestamp` | unix seconds | When Quo signed the request. | | `webhook-signature` | `v1,` (space-separated entries) | HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw-body}`. | Always verify the signature before trusting the body. See [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation). ## See also * [Quickstart](/docs/mdx/beta/webhooks-quickstart) * [Webhook event payloads](/docs/mdx/beta/webhooks-event-payloads) * [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation) * [Webhook API reference](/docs/mdx/beta/webhooks-api-reference) * [Migrating from legacy](/docs/mdx/beta/webhooks-differences-from-current) # Quickstart Source: https://www.quo.com/docs/mdx/beta/webhooks-quickstart Set up a beta webhook in five minutes. This is the beta webhook API, available in open beta. For the existing webhook system, see [Legacy webhooks](/docs/mdx/guides/webhooks). This guide takes you from zero to a verified, production-shaped webhook delivery in five minutes. Read [Overview](/docs/mdx/beta/webhooks-overview) for the underlying delivery semantics, payload anatomy, and versioning policy. ## Before you start You'll need: * A Quo Public API key with permission to manage webhooks. * An HTTPS endpoint you control. For local development, use a tunnel such as `ngrok` or `cloudflared`. * A runtime that gives you the **raw, unparsed request body**. This is required for signature verification — see [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation) for framework-specific notes. You can send a real, signed test event before any code is written using `POST /webhooks/:id/events/test`. See step 4 below. ## 1. Create the webhook Pin the subscription to the current API version with `x-quo-api-version`. The version is recorded once when the webhook is created and used for every subsequent delivery. ```bash theme={null} curl https://api.quo.com/webhooks \ -X POST \ -H "Authorization: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "x-quo-api-version: 2026-03-30" \ -d '{ "url": "https://example.com/webhooks/quo", "events": ["message.received"], "label": "Quickstart webhook" }' ``` Save the `key` field from the response — that's your `whsec_…` signing secret. Treat it like a credential: store it as an environment variable, never in source. ## 2. Receive the delivery Each delivery sets three headers and a JSON body. Verify the signature against the **raw bytes** of the body, then parse. | Header | Purpose | | ------------------- | -------------------------------------------------------- | | `webhook-id` | Stable delivery identifier. Use as your idempotency key. | | `webhook-timestamp` | Unix seconds when Quo signed the request. | | `webhook-signature` | Space-separated `v1,` entries. | ## 3. Verify and handle the event The Svix SDK accepts Quo's headers and `whsec_…` key format unchanged. Reject any delivery whose timestamp is more than five minutes off from your server clock to defeat replay. ```ts Node theme={null} import { Webhook } from "svix" import express from "express" const app = express() const secret = process.env.QUO_WEBHOOK_KEY ?? "" app.post( "/webhooks/quo", express.raw({ type: "application/json" }), (req, res) => { const deliveryId = req.header("webhook-id")! const headers = { "webhook-id": deliveryId, "webhook-timestamp": req.header("webhook-timestamp")!, "webhook-signature": req.header("webhook-signature")!, } const wh = new Webhook(secret) const event = wh.verify(req.body, headers) as { id: string; type: string } if (alreadyProcessed(deliveryId)) return res.status(200).end() markProcessed(deliveryId) handle(event) res.status(200).end() } ) ``` ```python Python theme={null} from svix.webhooks import Webhook from flask import Flask, request app = Flask(__name__) secret = os.environ["QUO_WEBHOOK_KEY"] @app.post("/webhooks/quo") def handle(): payload = request.get_data() # raw bytes — do not call request.get_json() delivery_id = request.headers["webhook-id"] headers = { "webhook-id": delivery_id, "webhook-timestamp": request.headers["webhook-timestamp"], "webhook-signature": request.headers["webhook-signature"], } event = Webhook(secret).verify(payload, headers) if already_processed(delivery_id): return "", 200 mark_processed(delivery_id) process(event) return "", 200 ``` Return `200` to acknowledge. Any non-`2xx` response triggers a retry. ## 4. Send a test event Trigger a real, signed delivery to your endpoint without waiting for a real call or message. The response also includes the sample payload inline so you can confirm what your endpoint received. ```bash theme={null} curl https://api.quo.com/webhooks/{id}/events/test \ -X POST \ -H "Authorization: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "x-quo-api-version: 2026-03-30" \ -d '{ "eventType": "message.received" }' ``` If verification fails, the most common cause is a framework that parsed the JSON before your handler saw the bytes. See [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation#framework-gotchas). ## 5. Inspect deliveries Every delivery is recorded. Use these endpoints to debug a missing or failing event: * `GET /webhooks/:id/events` — recent deliveries with status. * `GET /webhooks/:id/events/:eventId` — request body, all attempts, response codes. * `POST /webhooks/:id/events/:eventId/retry` — manually retry a failed delivery. ## Next steps * [Overview](/docs/mdx/beta/webhooks-overview) — delivery semantics, idempotency, retries, ordering. * [Webhook event payloads](/docs/mdx/beta/webhooks-event-payloads) — schemas and examples for every event type. * [Validate webhook signatures](/docs/mdx/beta/webhooks-signature-validation) — manual HMAC, framework gotchas, test vector. * [Migrating from legacy](/docs/mdx/beta/webhooks-differences-from-current) — numbered rollout if you have existing legacy webhooks. # Validate webhook signatures Source: https://www.quo.com/docs/mdx/beta/webhooks-signature-validation Verify signed webhook deliveries from Quo. This is the beta webhook API, available in open beta. For the existing webhook system, see [Legacy webhooks](/docs/mdx/guides/webhooks). ## Overview Quo signs every webhook delivery. Before you trust a payload, verify it by checking the signed headers against the **raw request body bytes**. Each request includes three headers: | Header | Meaning | | ------------------- | ---------------------------------------------------------- | | `webhook-id` | A stable identifier for this delivery. | | `webhook-timestamp` | Unix seconds when Quo signed the request. | | `webhook-signature` | A space-separated list of `v1,` entries. | The signature is HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw-body}`, encoded as base64. Verification must use the exact raw request body bytes Quo sent. If your middleware parses or rewrites the JSON body first, verification will fail. ## Webhook key format When you create a webhook, Quo returns a `key` value prefixed with `whsec_`. Store it exactly as returned. * The `whsec_` prefix is part of the canonical format; SDK-based verification accepts it as-is. * If you verify manually, strip `whsec_` and base64-decode the remainder to get the HMAC key bytes. ## SDK verification (recommended) You can verify Quo webhook signatures with the [`svix`](https://www.npmjs.com/package/svix) library — it accepts Quo's headers and `whsec_...` key format as-is. Install the library: ```bash theme={null} npm install svix ``` Then verify: ```ts theme={null} import { Webhook } from 'svix' const secret = process.env.QUO_WEBHOOK_KEY ?? '' // whsec_... const getHeader = (value: string | string[] | undefined, name: string): string => { if (typeof value === 'undefined') { throw new Error(`Missing required header: ${name}`) } if (Array.isArray(value)) { throw new Error(`Expected a single ${name} header value`) } return value } const headers = { 'webhook-id': getHeader(request.headers['webhook-id'], 'webhook-id'), 'webhook-timestamp': getHeader(request.headers['webhook-timestamp'], 'webhook-timestamp'), 'webhook-signature': getHeader(request.headers['webhook-signature'], 'webhook-signature'), } const webhook = new Webhook(secret) // Throws on error, returns the verified content on success. const verified = webhook.verify(rawBody, headers) ``` ## Manual verification If you prefer not to add an SDK dependency, verify with Node's built-in `crypto`: ```ts theme={null} import crypto from 'node:crypto' const secret = process.env.QUO_WEBHOOK_KEY ?? '' // whsec_... const MAX_AGE_SECONDS = 5 * 60 const secretBase64 = secret.startsWith('whsec_') ? secret.slice('whsec_'.length) : secret const secretBytes = Buffer.from(secretBase64, 'base64') const webhookId = request.headers['webhook-id'] const webhookTimestamp = request.headers['webhook-timestamp'] const webhookSignature = request.headers['webhook-signature'] if (!webhookId || !webhookTimestamp || !webhookSignature) { throw new Error('Missing required webhook headers') } const timestamp = Number(webhookTimestamp) const now = Math.floor(Date.now() / 1000) if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > MAX_AGE_SECONDS) { throw new Error('Invalid or stale webhook timestamp') } const signedContent = `${webhookId}.${webhookTimestamp}.${rawBody}` const expectedSignature = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64') const providedSignatures = webhookSignature .split(' ') .map((entry) => entry.trim()) .filter(Boolean) .map((entry) => { const [version, signature] = entry.split(',') return version === 'v1' ? signature : undefined }) .filter((signature): signature is string => Boolean(signature)) const isValid = providedSignatures.some((signature) => { const left = Buffer.from(signature) const right = Buffer.from(expectedSignature) return left.length === right.length && crypto.timingSafeEqual(left, right) }) if (!isValid) { throw new Error('Invalid webhook signature') } ``` ## Implementation notes * Always verify using the raw request body, before parsing or transforming it. * Reject deliveries whose `webhook-timestamp` is more than a few minutes off from the current time to protect against replay. * Store the webhook key (`whsec_...`) exactly as Quo returns it; do not trim, rewrap, or lowercase it. ## Other languages Any library that supports Quo's signing scheme — HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw-body}` with the `webhook-*` header set and a `whsec_...` base64 secret — can verify Quo webhooks. The Svix SDKs are a convenient off-the-shelf option across [several languages](https://docs.svix.com/receiving/verifying-payloads/how); you can also port the manual example above. # Building with AI LLMs Source: https://www.quo.com/docs/mdx/guides/building-with-ai-llms Learn how to use AI Language Models to build applications with the Quo API. ## Introduction Building with Large Language Models (LLMs) can significantly accelerate your Quo API integration development. This guide will help you effectively use LLMs to create applications with our API. While we provide examples using Claude, the principles and practices outlined here apply to any capable LLM platform. ## Getting started ### Documentation setup Before beginning development with an LLM, gather and prepare the necessary documentation: Get our [OpenAPI specification](https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json) for detailed endpoint information. **Tip**: Right-click and select "Save Link As..." to download the file Download and extract our [complete documentation package](https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-llm-ready-docs-prod.zip) Provide these resources to your LLM to help it understand Quo API capabilities ## Development process ### Working with LLMs Start by clearly describing your integration objectives to the LLM Share relevant API documentation and specifications Let the LLM help break complex features into manageable tasks Generate and review code one step at a time ### Best practices * Start with core functionality * Iterate to add features * Test each component thoroughly * Move forward only after validation * Never share API keys with LLMs * Keep sensitive data out of prompts * Validate all generated code * Follow security best practices * Follow Quo API rate limits * Implement proper error handling * Monitor API usage * Optimize API calls ## Example interactions Here's a practical example of how to instruct an LLM to help build with our API: ```text theme={null} I want to build an application that: 1. Displays a list of all my phone numbers 2. Allows me to select a number to send a message with 3. Lets me input an external phone number to send a message to 4. Sends a message to the external phone number Please help me implement this using the Quo API. ``` ```text theme={null} Help me create a system to: 1. Sync contacts from my CRM 2. Update contact details automatically 3. Track message history per contact 4. Generate contact activity reports ``` ## Integration patterns Automate message handling and responses Sync and manage contact information Process call summaries and recording data Manage scheduling and reminders ## Implementation checklist Thoroughly review all LLM-generated code Test extensively in a development environment Implement comprehensive error handling and logging Deploy with appropriate monitoring systems Continuously improve based on usage and feedback Need more detailed guidance? Check out our comprehensive [API Reference](/docs/api-reference/introduction) for detailed endpoint documentation and examples. # External contacts Source: https://www.quo.com/docs/mdx/guides/contacts Learn about working with contacts imported via the Quo API or native integrations. ## Understanding external contacts External contacts are contacts that originate from outside the Quo app: either created through the Quo API or synced via native integrations (such as CRM or other connected platforms). These contacts allow you to centralize contact records from multiple sources within your Quo workspace. ### Key characteristics External contacts are separate from contacts created directly in the Quo app, with their own specific behaviors depending on how they were created. External contacts can come from two sources: the Quo API or native integrations and each has different editability rules within the Quo app. ### Editability by source Contacts created via the Quo API can be updated directly within the Quo app. You can also continue managing them programmatically through API endpoints. Contacts synced from native integrations remain read-only within Quo. Any changes must be made in the source system and synced back to Quo. ### Important behaviors and limitations When you create a contact using the `POST /contacts` endpoint, it's essential to save the `id` returned in the response. This `id` will be required for all future API operations involving the contact. After creating an API contact, it will only appear in the Quo app—whether in the conversation list, contact list, or search results—if there's an associated conversation with a matching phone number. ## Contact field structure Quo organizes contact information using two distinct field types. Understanding these is crucial for effective contact management. Every contact in Quo includes these predefined fields: * First Name * Last Name * Role * Company * Emails * Phone Numbers These fields maintain consistent properties across all contacts and form the foundation of contact information. Custom fields allow for flexible, user-defined contact properties. Supported data types include: * Address * Boolean * Date * Multi-select * Number * String * URL **Managing Custom Fields:** Custom field definitions can only be modified within the Quo app. The API does not currently support creating or editing custom field definitions. ## Creating and managing contacts via API Follow these steps to effectively create and manage contacts through the API: First, call the `GET /contact-custom-fields` endpoint to retrieve your workspace's custom contact field definitions. Structure your contact data according to both default and custom fields: ```json theme={null} { "defaultFields": { "firstName": "John", "lastName": "Doe", "phoneNumbers": [ { "name": "primary", "value": "+1234567890" } ] }, "customFields": { // Include any custom field values here } } ``` Use the `POST /contacts` endpoint to create the contact and store the returned contact ID for future operations. Update contacts either within the Quo app or programmatically using the `PATCH /contacts/:id` endpoint with the saved contact ID. Always validate phone numbers are in E.164 format (+1234567890) before creating or updating contacts to ensure proper functionality. # Sync your contacts Source: https://www.quo.com/docs/mdx/guides/sync-contacts Implement a one-way contact sync from Google Sheets to Quo using Javascript. ## Overview This guide provides a foundation for implementing a one-way sync from Google Sheets to Quo using JavaScript. You may need to adjust some details based on your specific requirements and environment. Remember to thoroughly test the implementation to ensure data integrity. ## Development guide ##### 1.1 Quo API. * Obtain your Quo API key from the Quo dashboard. ##### 1.2 Google Sheets API * Enable the Google Sheets API in your Google Cloud Console. * Create service account credentials and download the JSON key file. ##### 1.3 Google Sheets * Create a Google Sheet in the following format: | contactId | firstName | lastName | phone | email | | --------- | --------- | -------- | -------------- | ------------------------------------------- | | | Jane | Doe | (555) 555-5555 | [jane@example.com](mailto:jane@example.com) | * Share your Google Sheet with the service account email address. ##### 2.1 Ensure you have Node.js installed on your system. ##### 2.2 Create a new Node.js project and initialize it ```bash theme={null} mkdir quo-sync cd quo-sync npm init -y ``` ##### 2.3 Install required packages ```bash theme={null} npm install googleapis axios dotenv node-cron ``` ##### 2.4 Create a .env file to store environment variables ```bash theme={null} QUO_API_KEY=your_quo_api_key GOOGLE_APPLICATION_CREDENTIALS=path/to/your/credentials.json GOOGLE_SHEET_ID=your_google_sheet_id ``` ##### 3.1 Create a new file named `sync.js` and add the setup functions ```js theme={null} require("dotenv").config(); const { google } = require("googleapis"); const axios = require("axios"); const cron = require("node-cron"); const API_BASE_URL = "https://api.quo.com/v1"; const quo = axios.create({ baseURL: API_BASE_URL, headers: { Authorization: process.env.QUO_API_KEY, "Content-Type": "application/json", }, }); const googleAuth = new google.auth.GoogleAuth({ keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS, scopes: ["https://www.googleapis.com/auth/spreadsheets"], }); ``` ##### 3.2 Add the Quo API helper functions ```js theme={null} async function createQuoContact(contactData) { const response = await quo.post("/contacts", contactData); return response.data.data; } async function updateQuoContact(contactId, contactData) { const response = await quo.patch(`/contacts/${contactId}`, contactData); return response.data.data; } ``` ##### 3.3 Add the Google Sheets to Quo contacts mapping function ```js theme={null} function mapFields(sheetRow) { if (!sheetRow.firstName) { console.warn("Missing required firstName in row: ", sheetRow); return; } return { defaultFields: { firstName: sheetRow.firstName, lastName: sheetRow.lastName, phoneNumbers: sheetRow.phone ? [{ name: "primary", value: sheetRow.phone }] : undefined, emails: sheetRow.email ? [{ name: "primary", value: sheetRow.email }] : undefined, }, }; } ``` ##### 3.4 Add the Google Sheets helper functions ```js theme={null} async function getGoogleSheetsData() { const sheets = google.sheets({ version: "v4", googleAuth }); const response = await sheets.spreadsheets.values.get({ spreadsheetId: process.env.GOOGLE_SHEET_ID, range: "Sheet1!A1:Z", // First sheet and all initial columns }); const rows = response.data.values; const headers = rows[0]; // First row contains headers return rows.slice(1).map((row) => { // Skip the first row and map the contact data const contact = {}; headers.forEach((header, index) => { contact[header] = row[index]; }); return contact; }); } async function updateSheetWithContactId(rowNumber, contactId) { const sheets = google.sheets({ version: "v4", googleAuth }); await sheets.spreadsheets.values.update({ spreadsheetId: process.env.GOOGLE_SHEET_ID, range: `Sheet1!A${rowNumber + 2}`, // +2 to account for 1-based index and header row valueInputOption: "RAW", resource: { values: [[contactId]] }, }); } ``` ##### 3.4 Finally, tie it all together ```js theme={null} async function syncContacts() { const sheetContacts = await getGoogleSheetsData(); for (const [rowNumber, sheetRow] of sheetContacts.entries()) { const mappedContact = mapFields(sheetRow); if (sheetRow.contactId) { await updateQuoContact(sheetRow.contactId, mappedContact); } else { const { id } = await createQuoContact(mappedContact); await updateSheetWithContactId(rowNumber, id); } } console.log("Sync completed successfully"); } // Run sync every hour cron.schedule("0 * * * *", syncContacts); console.log("Sync process started. Running every hour."); ``` ```bash theme={null} node sync.js ``` This will start the sync process, which will run every hour. ## Considerations and Optimizations * Implement deletion logic to remove contacts from Quo that are no longer present in the Google Sheet. * Implement pagination for fetching Quo contacts if you have a large number of contacts. * Implement more robust error handling and retry mechanisms. * Implement logging for auditing and troubleshooting purposes. * Consider using a database to store the state of the sync process and to track changes between syncs. * Consider implementing rate-limiting and an incremental sync to reduce API calls and processing time. * For production use, consider deploying this script to a cloud platform like Heroku or AWS Lambda for better reliability and scalability. # Webhook payload reference Source: https://www.quo.com/docs/mdx/guides/webhooks A reference for API-generated webhook payloads. ## Overview Quo API webhooks allow developers to receive real-time notifications for various events, such as calls, messages, and transcripts. By integrating webhooks into your workflows, you can automate processes, enhance user experiences, and seamlessly connect Quo with other systems. **Important note:** Webhooks created in the Quo app are not compatible with those created via the API. You cannot access or modify app webhooks through the API, or API webhooks in the app. ## Webhooks payload sample data models Each webhook event provides a structured payload with specific data. We've provided sample payloads below for the most common webhook events. ### Calls These webhooks are triggered in response in response to call-related events: `call.ringing`, `call.completed`, and `call.recording.completed`. The following is an example of the payload for a `call.ringing` event. ```json theme={null} { "id": "EV0ea54cadfbf342e6ac4ca1f22ed1700c", "object": "event", "apiVersion": "v4", "createdAt": "2022-06-24T19:35:46.825Z", "type": "call.ringing", "data": { "object": { "id": "ACsXlF0", "object": "call", "answeredAt": "2022-01-01T00:00:00Z", "answeredBy": "USlHhXmRMz", "initiatedBy": "USlHhXmRMz", "direction": "outgoing", "status": "ringing", "completedAt": "2022-01-01T00:10:00Z", "createdAt": "2022-01-01T00:00:00Z", "duration": 60, "forwardedFrom": "UShYmRNzlm", "forwardedTo": "UShXmRMzln", "phoneNumberId": "PN1ZmRMzlx", "participants": [ "+15555555555" ], "updatedAt": "2022-01-01T00:00:00Z", "userId": "USlHhXmRMz", "contactIds": [ "6824dfb69aee85c132b7dg65" ] } } } ``` ### Call Summaries This webhook is triggered in response to a `call.summary.completed` event. ```json theme={null} { "id": "EV0ea54cadfbf342e6ac4ca1f22ed1700c", "object": "event", "apiVersion": "v4", "createdAt": "2022-06-24T19:35:46.825Z", "type": "callSummary", "data": { "object": { "callId":"AC16558bc5f73445598a2627f5a94fe014", "object": "callSummary", "status": "completed", "summary": [ "You talked about the weather." ], "nextSteps": [ "Bring an umbrella." ], "contactIds": [ "6824dfb69aee85c132b7dg65" ] } } } ``` ### Call Transcripts This webhook is triggered in response to a `call.transcript.completed` event. ```json theme={null} { "id": "EV0ea54cadfbf342e6ac4ca1f22ed1700c", "object": "event", "apiVersion": "v4", "createdAt": "2022-06-24T19:35:46.825Z", "type": "callTranscript", "data": { "object": { "callId": "AC16558bc5f73445598a2627f5a94fe014", "object": "callTranscript", "createdAt": "2022-06-24T19:34:50.279Z", "dialogue": [ { "content": "Hello, world!", "start": 5.123456, "end": 10.123456, "identifier": "+19876543210", "userId": "USlHhXmRMz" } ], "duration": 5, "status": "completed", "contactIds": [ "6824dfb69aee85c132b7dg65" ] } } } ``` ### Messages This webhook is triggered in response to message events such as `message.received` and `message.delivered`. Below is a sample payload for a `message.received` event. ```json theme={null} { "id": "EVc67ec998b35c41d388af50799aeeba3e", "object": "event", "apiVersion": "v4", "createdAt": "2022-01-23T16:55:52.557Z", "type": "message.received", "data": { "object": { "id": "AC24a8b8321c4f4cf2be110f4250793d51", "object": "message", "from": "+19876543210", "to": ["+15555555555"], "direction": "incoming", "text": "Hello, world!", "status": "delivered", "createdAt": "2022-01-23T16:55:52.420Z", "userId": "USu5AsEHuQ", "phoneNumberId": "PNtoDbDhuz", "contactIds": [ "6824dfb69aee85c132b7dg65" ] } } } ``` # Contact the team Source: https://www.quo.com/docs/mdx/pricing-support/contact-the-team Stuck? Need Help? Drop us a line at [support+developers@quo.com](mailto:support+developers@quo.com). # Tips for minimizing costs Source: https://www.quo.com/docs/mdx/pricing-support/minimizing-costs We’ve provided the below tips to help you minimize segment counts and save money. Standard Latin alphabet characters, numbers, and basic punctuation use 160 characters per segment. Some special characters (é, ñ, ß) reduce segment capacity to 70 characters. Most emojis count as two characters. Extensive use can quickly increase segment count. Each line break counts as a character. Long URLs can span multiple segments. Shortened links save space. Use widely understood abbreviations to reduce character count. # API Pricing overview Source: https://www.quo.com/docs/mdx/pricing-support/pricing-overview Welcome to Quo's simple and transparent API pricing structure. ## Our pricing philosophy At Quo, we believe in transparent and fair pricing. Sometimes it can be hard to understand the true cost of something when it's buried in the fine print. We are committed to helping you clearly understand and manage your costs when using our platform. ## Core pricing model Our pricing is based on message segments, making it easy to calculate costs:

\$0.01 per segment

\$0.01 + country-specific rate per segment

Rates vary by destination country

For detailed international rates by country, see our [International Pricing Guide](https://www.openphone.com/rates). ## Understanding message segments A **segment** is the basic unit we use to calculate SMS billing. Every message you send is divided into one or more segments based on two factors: 1. **Message length** (character count) 2. **Character type** (standard or special characters) ## How character types affect segment size ### Standard GSM characters Messages using only standard GSM-7 characters fit **up to 160 characters per segment**. Standard characters include: * Letters (A-Z, a-z) * Numbers (0-9) * Spaces * Basic punctuation (. , ! ? - etc.) ### Special/Non-GSM characters **If your message contains even one special character, the entire message is billed at the 70-character limit** — not just the portion with special characters. This often results in more segments and higher costs than you might expect. Messages containing **any** special characters fit only **up to 70 characters per segment**. Special characters include: * Accented letters (é, ñ, ü) * Curly/smart quotes (" " ' ') * Emojis (😊, 🚀, ✨) * Many international characters ## Tools and optimization ### Segment calculator Use our [Segment Calculator](https://twiliodeved.github.io/message-segment-calculator/) tool to estimate costs before sending messages. This helps you optimize your message length and content to avoid unexpected charges. ### Smart encoding The Quo API automatically enables **smart encoding** to minimize segment usage and reduce costs wherever possible. This feature works behind the scenes to choose the most efficient encoding for your messages. ## API message types You're only charged for outgoing API-powered messages, which include: Messages sent through direct API calls Messages sent via applications built with our API ## How our billing works We use a credit-based system for all API messaging charges: Add funds to your account through the "Plans & Billing" tab Credits are automatically deducted when messages are sent Monitor your balance and enable auto-recharge to prevent service interruptions ## Important service notes ### Requirements & limitations 1. An active Quo subscription is required for API access. 2. MMS is not supported in the current API version. 3. Partial credits cannot be used for sending messages; the API will return an error. If your credit balance is insufficient for a message's full cost, the API will return an error and the message won't be sent. ### Support & assistance Need help understanding our pricing or managing your costs? Our team is here to help. Email us at [support+developers@quo.com](mailto:support+developers@quo.com) with any questions. # Terms of Service Source: https://www.quo.com/docs/mdx/pricing-support/terms-of-service Developer API Terms of Service Please read these Developer API Terms of Service (the “Agreement”) carefully before using the API and Service (each as defined below) offered by OpenPhone Technologies, Inc. (“OpenPhone”). By clicking on the “Accept” or “Submit” button, you or the entity or company that you represent (“You,” “Your,” “Yours” or “Developer”) are unconditionally consenting to be bound by and are becoming a party to this Agreement. Your use of any portion of the API or Service, as well as your submission of any registration form or similar document that references this Agreement shall also constitute assent to this Agreement. If you do not unconditionally agree to all of the terms of this Agreement, click the “Decline” button and you shall have no right to use the API or Service. If you are entering into this Agreement on behalf of an entity, then you represent and warrant that you are authorized to bind such entity to the terms of this Agreement. If the terms of this Agreement are considered an offer, acceptance is expressly limited to such terms. WHEREAS, OpenPhone owns and operates a business phone, texting, and collaborative workspace system and application (the “Service”); WHEREAS, Developer desires to acquire from OpenPhone, and OpenPhone desires to provide to Developer, the right and license to access and use certain technologies and develop integrations to the Services as more fully described herein; NOW THEREFORE, the parties hereto, in consideration of the foregoing and other good and valuable consideration recognized by the parties, hereby agree as follows: ​ 1. Definitions The following terms shall have the following meanings for the purpose of this Agreement: 1.1 “Acceptable Use Policy” means OpenPhone’s acceptable use policy for Developers creating Integrations, available at [https://www.openphone.com/terms](https://www.openphone.com/terms), which may be updated by OpenPhone from time to time. 1.2 “API” means OpenPhone’s application programming interfaces and specifications thereto, as it is provided by OpenPhone to Developer, to enable Developer and End Users to interface with the Service. 1.3 “Documentation” means documentation and information regarding the API and Service that are delivered by OpenPhone to Developer in any form (including the documentation set forth at [https://www.openphone.com/docs](https://www.openphone.com/docs), including any updates to such documentation provided by OpenPhone from time to time. 1.4 “End User” means a user that accesses the API or the Service through the Integration for such user’s own benefit. 1.5 “End User Content” means any information, data, text, content or other materials that Developer or End Users upload, submit, transmit, display, post, store, or otherwise make available through the Service, including through the API or the Integration. 1.6 “Integration” means a software application or process that utilizes the API to make the Services compatible and/or interoperable with another software application or platform. 1.7 “OpenPhone Data” means any information, data, text or other content provided by or on behalf of OpenPhone to Developer about an individual End User. 1.8 “OpenPhone Terms of Service” means OpenPhone’s standard terms of service, available at [https://www.openphone.com/terms](https://www.openphone.com/terms), which may be updated by OpenPhone from time to time. ​ 2\. API, Service and OpenPhone Data License; Restrictions 2.1 License. Subject to the terms and conditions of this Agreement, OpenPhone hereby grants Developer a non-exclusive, non-transferable, non-sublicensable, revocable, and limited right and license during the Term to access and use the Service, API and OpenPhone Data (a) to build one or more Integrations that connect to the Service, and (b) to permit End Users who have agreed to the OpenPhone Terms of Service to access the Service via such Integrations, in each case, in accordance with the Documentation, Acceptable Use Policy, and the OpenPhone Terms of Service. For the avoidance of doubt, Developer may not make any Integration or provide access to the Service to any End User or any other person or entity who has not agreed to the OpenPhone Terms of Service. 2.2 Registration; Monetization. Prior to accessing the API or developing an Integration, Developer shall complete OpenPhone’s standard registration process and provide all requested information, including, without limitation, (i) contact information for Developer, (ii) the purpose, features, and functionality of the Integration, and (iii) whether Developer intends to charge End Users for or otherwise monetize the Integration. Developer may not charge End Users for access to the Services via the Integration or otherwise monetize the Integration without OpenPhone’s prior written approval. 2.3 Responsibilities. Developer is solely responsible for the acts or omissions of Developer and each End User in connection with their use of the API and Service in connection with the Integration. Developer’s agreements with End Users must: (i) be no less protective of OpenPhone’s rights and ownership than this Agreement and the OpenPhone Terms of Service; (ii) not grant greater use or access rights to the Service or API than those rights, licenses and permissions described in OpenPhone Terms of Service; (iii) include substantially and materially similar restrictions to those set forth in Section 2.5 with respect to the Service and API to the extent applicable; and (iv) require, as a condition of accessing the Services via the Integration, that End Users have agreed to the OpenPhone Terms of Service. Developer shall prohibit unauthorized access to or use of the API and to promptly notify OpenPhone of any such unauthorized access or use. Developer accepts and assumes all responsibility for complying with all applicable laws and regulations in connection with all of Developer’s and End Users’ activities involving the API, the Service, End User Content and OpenPhone Data. 2.4 Updates and Modifications. Developer understands and agrees that the specifications for the API and the Service shall be defined by OpenPhone in its sole discretion, and Developer is responsible for its development and other costs associated with Developer’s use of the API or Integration. OpenPhone reserves the right to modify, change, update and/or enhance the API and/or the Service (each a “Modification”) at any time in OpenPhone’s sole and exclusive discretion. Developer acknowledges and agrees that such Modifications may affect Developer’s and End Users’ ability to access the Service and may require Developer to make changes to the Integration. OpenPhone shall not be liable for any costs incurred by Developer arising out of or in connection with any Modification. 2.5 License Restrictions. 2.5.1 Except as expressly permitted hereunder, Developer shall not, and shall require that End Users do not (i) use any method to access or use the Service other than as permitted through the API, (ii) provide the API or access to the Service to any third parties other than End Users, (iii) permit or enable third parties to copy or obtain the API or access to the Service in any manner not expressly authorized in this Agreement, (iv) use the API or Service, in any manner that violates applicable laws, (v) license, sell, re-sell, rent, lease, transfer, assign, reproduce, distribute, or alter the API, Service or any portion of the API or Service, or permit or enable any third parties to do so; (vi) use the Service, the API, or any documentation or other materials received from OpenPhone in connection with this Agreement, to develop a product or service that competes with the Service; (vii) modify, translate, adapt, merge, make derivative works of, disassemble, decompile, reverse compile or reverse engineer, or otherwise attempt to discover the source code or underlying algorithms of any part of the Service or API except to the extent the foregoing restrictions are expressly prohibited by applicable law; (viii) remove or destroy any copyright notices or other proprietary markings contained on or in the Service or API; (ix) access or use the API or Service in any manner that could disable, overburden, damage, disrupt or impair the API or Service or interfere with any other party’s access to or use of the API or Service or use any device, software or routine that causes the same; (x) attempt to gain unauthorized access to, interfere with, damage or disrupt the API or Service, accounts registered to other users, or the computer systems or networks connected to the API or Service; (xi) circumvent, remove, alter, deactivate, degrade or thwart any technological measure or content protections of the API or Service; (xii) use any robot, spider, crawlers, scraper, or other automatic device, process, software or queries that intercepts, “mines,” scrapes, extracts, or otherwise accesses the API or Service to monitor, extract, copy or collect information or data from or through the API or Service; or (xiii) introduce any viruses, trojan horses, worms, logic bombs or other materials that are malicious or technologically harmful into OpenPhone’s systems. 2.6 Public Announcement. The timing and content of any advertisements, announcements, press releases or other promotional activity relating to this Agreement, and the use of one party’s name or trademarks by the other party shall be subject to the prior approval of both parties. Notwithstanding the foregoing, OpenPhone may reference Developer as a Developer in advertisements, press releases, or other marketing or promotional activities regarding OpenPhone’s products or services. ​ 3\. Ownership; Licenses; Third-Party Materials 3.1 OpenPhone Ownership. As between OpenPhone and Developer, OpenPhone retains all rights, title and interest in and to all intellectual property rights embodied in or pertaining to the API, Service, OpenPhone Data, and OpenPhone Marks (as defined in Section 3.3), and all improvements, modifications, enhancements, and derivative works of any of the foregoing. There are no implied licenses under this Agreement, and any rights not expressly granted to Developer hereunder are reserved by OpenPhone or its licensors. Developer shall not take any action inconsistent with OpenPhone’s ownership of the API, Service, OpenPhone Data, or OpenPhone Marks. 3.2 Integration; End User Content. As between OpenPhone and Developer, to the extent permitted by applicable law, Developer retains ownership of the Integration, subject to the license granted to OpenPhone in the following sentence. Developer grants OpenPhone a perpetual, non-exclusive, sublicensable (through multiple tiers of sublicensees) royalty-free, fully paid right and license to use, copy, host, store, transfer, display, perform, reproduce, modify for the purpose of formatting for display, and distribute the Integration and End User Content, in whole or in part, in any and all media or distribution methods (now known or later developed) for the purposes of operating and providing the Service to Developer and End Users, and any for any other lawful business purpose, including to improve the usability, functionality, and accuracy of the Service. 3.3 Trademark License. Developer will prominently include the words \[“Powered by OpenPhone”] wherever it makes the Service available to End Users (including in the end-user facing interface of the Integration) and in all marketing and promotional materials that reference the functionality provided by the Service or Integration. OpenPhone hereby grants Developer a limited, non-exclusive, non-transferable, non-sublicensable, royalty-free license to use OpenPhone’s trademarks, service marks, and logos (collectively “OpenPhone Marks”) during the Term on Developer’s websites or promotional materials solely to (i) attribute OpenPhone as the provider of the Service and (ii) otherwise advertise and promote the availability of access to the Service in the Integration. Developer agrees to use the OpenPhone Marks only in a form identified by OpenPhone in writing for use hereunder and such quality standards as may be reasonably established by OpenPhone and communicated to Developer from time to time in writing. Developer shall obtain OpenPhone’s prior written approval of any material change in the style and manner in which any of the OpenPhone Marks are proposed to be used. Developer shall not use the OpenPhone Marks in a manner that disparages OpenPhone or its products or services, portrays OpenPhone in a false, competitively adverse or poor light, or dilutes the OpenPhone Marks. Except as expressly provided for in this Section 3.3, OpenPhone reserves all right, title, and interest in and to the OpenPhone Marks. All goodwill arising from Developer’s use of the OpenPhone Marks shall inure to the benefit of OpenPhone. Developer hereby grants OpenPhone a limited, non-exclusive, non-transferable, non-sublicensable, royalty-free license to use Developer’s trademarks, service marks, and logos (collectively “Developer Marks”) on OpenPhone’s websites or promotional materials solely to advertise and promote the availability of access to the Service in the Integration and in accordance with Section 2.6. OpenPhone shall obtain Developer’s prior written approval of any material change in the style and manner in which any of the Developer Marks are proposed to be used. Developer shall not use the Developer Marks in a manner that disparages Developer or its products or services, portrays Developer in a false, competitively adverse or poor light, or dilutes the Developer Marks. Developer reserves all right, title, and interest in and to the Developer Marks. All goodwill arising from OpenPhone’s use of the Developer Marks shall inure to the benefit of Developer. 3.4 Feedback. Developer agrees that submission of any ideas, suggestions, documents, proposals or other feedback provided to OpenPhone (“Feedback”) is at Developer’s own risk and that OpenPhone has no obligations (including obligations of confidentiality) with respect to such Feedback. Developer represents and warrants that it has all rights necessary to submit the Feedback. Developer hereby grants to OpenPhone a fully paid, royalty-free, perpetual, irrevocable, worldwide, non-exclusive, transferrable, and fully sublicensable right and license to use, reproduce, perform, display, distribute, adapt, modify, re-format, create derivative works of, and otherwise exploit in any manner, any and all Feedback without restriction of compensation. 3.5 Content and Data. 3.5.1 End User Content. Developer represents and warrants that (i) before any End User may engage with the Integration or Service, Developer shall ensure that it provides all notices and obtains all consents required under applicable law to enable OpenPhone to process End User Content in accordance with OpenPhone’s privacy policy (currently available at [https://www.openphone.com/privacy](https://www.openphone.com/privacy)); (ii) it has sufficient rights, consents, and permissions to grant the licenses to OpenPhone set forth in Section 3.2 and to input the End User Content into the Service and (iii) the End User Content does not infringe, misappropriate, or otherwise violate any third party’s intellectual property rights, privacy rights, rights of publicity, moral rights, or other proprietary rights. Developer shall not (i) make representations or other statements with respect to End User Content that are contrary to or otherwise inconsistent with OpenPhone’s privacy policy or (ii) interfere with any independent efforts by OpenPhone to provide End User notice or obtain End User consent. 3.5.2 OpenPhone Data. OpenPhone Data shall only be used for the purpose of making the Service available to End Users in accordance with the OpenPhone Terms of Service and Developer shall delete all OpenPhone Data in accordance with the Documentation. Developer shall be responsible for obtaining consent directly from End Users for any other use of the End User’s information or data. To the extent that End User submits any information or data directly to Developer, Developer shall be solely responsible for ensuring that Developer’s use of that data is in compliance with any applicable laws and Developer’s own stated privacy policy. 3.5.3 DMCA. OpenPhone complies with the Digital Millennium Copyright Act (the “DMCA”) with regard to End User Content and all other content uploaded, submitted, transmitted, displayed, posted, stored, or otherwise made available on the Service that allegedly violates a third party’s copyright. OpenPhone reserves the right to delete or disable any content alleged to be infringing, and to terminate access to the Services for repeat alleged infringers. OpenPhone’s complete Copyright Dispute Policy is available at \[insert link]. 3.6 Third Party Services. Developer acknowledges and agrees that: (i) the Service may incorporate certain information, data, and materials from third party providers (collectively, “Third Party Services”), including without limitation through integrations or connectors to such Third Party Services that are provided by OpenPhone; (ii) Third Party Services may only be used in conjunction with the Service; and (iii) Developer’s use of the Third Party Services hereunder shall be subject to (and Developer agrees it is bound by) the third party terms and conditions referenced at Third Party Terms [https://www.openphone.com/terms](https://www.openphone.com/terms) (the “Third Party Terms Site”), as they may be modified from time to time by OpenPhone and/or its third party licensors or suppliers at any time in accordance with this Section 3.6 (collectively, the “Third Party Terms”), and which are incorporated into this Agreement by reference. In the event that OpenPhone makes any update to the Third Party Terms, OpenPhone shall use reasonable efforts to notify Developer of such update (email to suffice) at least 2 weeks in advance, which notice shall describe the applicable update, as well as the effective date of such update (which shall be at least 2 weeks after the date of such notice). Provided that OpenPhone has followed the foregoing procedure, any use by Developer of the Service following the effective date of an update to the Third Party Terms shall constitute acceptance of such update. OpenPhone does not make any representations or warranties with respect to Third Party Services or any third party providers. OpenPhone cannot and does not guarantee that the Service shall incorporate (or continue to incorporate) any particular Third Party Services. ​ 4\. Term and Termination 4.1 Term. This Agreement shall commence upon Developer’s first use of the API and/or Service and shall continue until terminated in accordance herewith. 4.2 Termination. OpenPhone may terminate this Agreement at any time for any reason or no reason at all upon ten (10) days’ written notice. Developer may terminate this Agreement at any time for any reason or no reason at all upon thirty (30) days’ written notice. Either party may terminate this Agreement immediately upon written notice to the other party (a) if the other party breaches any warranty, representation, covenant or obligation under this Agreement (or, in the case of Developer, the OpenPhone Terms of Service) and, if such breach is curable, fails to cure such breach within ten (10) days after receiving written notice of the breach from the non-breaching party; or (b) if the other party is subject to a dissolution, receivership, liquidation, insolvency, conservatorship, consolidation, reorganization, sale of substantially all of its assets, cessation of business, voluntary or involuntary bankruptcy. OpenPhone immediately may suspend Developer’s or any End User’s access to the API or Service or terminate this Agreement (i) if Developer monetizes the Integration without OpenPhone’s prior approval, (ii) if required to do so by law, or (iii) if OpenPhone determines such action is necessary to prevent a security risk or other creditable risk of harm or liability to OpenPhone, the Service, the API, or any third parties. 4.3 Effect of Termination; Survival\*\*.\*\* The provisions of Sections 1, 2.5, 3, 4.3, 5, 6.2, 7, 8 and 9 shall survive any expiration or termination of this Agreement. All other rights and obligations of the parties shall cease upon expiration or termination of this Agreement, and Developer shall cease use of the API, Service and OpenPhone Data as of the effective date of termination. ​ 5\. Confidential Information 5.1 Confidential Information. Each party and their respective affiliates, directors, officers, employees, authorized representatives, agents and advisors (including attorneys, accountants, consultants, bankers and financial advisors) shall keep confidential all proprietary information concerning the other party’s business procedures, present and future products, services, operations, marketing materials, fees, technology, policies or plans of the other party that is received or obtained during the negotiation or performance of the Agreement, whether such information is oral or written, and whether or not labeled as confidential by such party (collectively “Confidential Information”). 5.2 Use of Confidential Information. For as long as Confidential Information of the disclosing party is in possession of the receiving party, the receiving party shall take reasonable steps, at least substantially equivalent to the steps it takes to protect its own proprietary information, to prevent the use, duplication or disclosure of Confidential Information other than in accordance with this Agreement. Each party may disclose Confidential Information of the other party to its employees or agents who are directly involved in negotiating or performing this Agreement and who are apprised of their obligations under this Section and directed by the receiving party to treat such information confidentially, or as required by law or by a supervising regulatory agency of a receiving party. Neither party shall disclose, share, rent, sell or transfer to any third party any Confidential Information of the other party except as expressly permitted by this Agreement. The receiving party shall use Confidential Information of the other party only as necessary to perform this Agreement. 5.3 Exceptions. Notwithstanding anything to the contrary, the obligations of the receiving party set forth in this Section 5 shall not apply to any information of the disclosing party that: (a) is or becomes a part of the public domain through no wrongful act of the receiving party; (b) was in the receiving party’s possession free of any obligation of confidentiality at the time of the disclosing party’s communication thereof to the receiving party; (c) is developed by the receiving party completely independent from the Confidential Information of the disclosing party; or (d) is required by law or regulation to be disclosed, but only to the extent and for the purpose of such required disclosure after providing the disclosing party with advance written notice if reasonably possible such that the disclosing party is afforded an opportunity to contest the disclosure or seek an appropriate protective order. 5.4 Remedies. Upon the request of the disclosing party following the termination of this Agreement, the other party shall promptly return all Confidential Information of the disclosing party in its possession, and shall promptly destroy such materials containing such information (and any copies, extracts, and summaries thereof) and shall further provide the other party with written confirmation of such return or destruction upon written request. In the event a party discovers that Confidential Information of the other party has been used in an unauthorized manner or disclosed in violation of this Section 5, the party discovering the unauthorized use or disclosure shall promptly notify the other party of such event. In addition, the non-disclosing party shall be entitled to all other remedies available at law or equity, including injunctive relief. ​ 6\. Limited Representations and Warranties 6.1 General. Each party represents and warrants that (i) it is a duly incorporated or organized entity in its state of incorporation or organization and that it has the full power and authority to enter into and perform its obligations under this Agreement; (ii) the execution and performance by it of its obligations under this Agreement do not constitute a breach of or conflict with any other agreement or arrangement by which it is bound; (iii) this Agreement is a legal, valid and binding obligation of the party executing this Agreement; (iv) no consent or approval of any other party is required in connection with the execution, delivery, performance, or enforceability of this Agreement; and (v) it shall comply with all applicable laws, rules, and regulations in connection with performance of such party’s obligations under this Agreement. 6.2 Warranty Disclaimer. EXCEPT FOR THE REPRESENTATIONS AND WARRANTIES EXPRESSLY SET FORTH IN THIS SECTION 6, THE API, SERVICE, AND OPENPHONE DATA ARE EACH PROVIDED “AS IS” AND OPENPHONE AND ITS LICENSORS EXPRESSLY DISCLAIM ALL WARRANTIES AND CONDITIONS, EXPRESS, IMPLIED OR STATUTORY, INCLUDING THE IMPLIED WARRANTIES FOR TITLE, NON-INFRINGEMENT, MERCHANTABILITY, QUIET ENJOYMENT, FITNESS FOR A PARTICULAR PURPOSE, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR TRADE USAGE. OPENPHONE DOES NOT REPRESENT OR WARRANT THAT (I) THE API, OR THE SERVICE SHALL MEET DEVELOPER’S REQUIREMENTS (SUCH AS THE QUALITY, EFFECTIVENESS, REPUTATION AND OTHER CHARACTERISTICS OF THE API AND SERVICE); (II) DEVELOPER’S OR ITS USERS’ USE OF THE API AND SERVICE SHALL BE UNINTERRUPTED, TIMELY, SECURE OR ERROR-FREE; OR (III) THE ADVICE, RESULTS, OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM USE OF THE SERVICE OR API SHALL BE ACCURATE OR RELIABLE. DEVELOPER ACKNOWLEDGES THAT THE SERVICE MAY INCLUDE THIRD PARTY SERVICES AND THAT OPENPHONE IS NOT LIABLE, AND DEVELOPER AGREES NOT TO SEEK TO HOLD OPENPHONE LIABLE, FOR ANY THIRD PARTY SERVICES, AND THAT THE RISK OF INJURY FROM SUCH THIRD PARTY SERVICES RESTS ENTIRELY WITH DEVELOPER. ​ 7\. Indemnification Developer agrees to indemnify, defend and hold harmless OpenPhone, and parents, subsidiaries, affiliates, officers, employees, agents, partners, suppliers, and licensors, from and against any and all third-party losses, costs, liabilities, and claims (including reasonable attorneys’ fees) relating to or arising out of (a) Developer’s use or misuse of the API, Service, OpenPhone Data or intentional misconduct; (b) Developer’s violation of this Agreement; (c) Developer’s violation of any applicable law, rule or regulation; and (d) Developer’s violation of any other party’s right, including without limitation any right of privacy or intellectual property rights. Developer may not enter into any settlement or compromise of any such claim without prior written consent of OpenPhone. OpenPhone reserves the right, at its own cost, to assume the exclusive defense and control of any matter otherwise subject to indemnification by Developer, in which event Developer shall fully cooperate with OpenPhone in asserting any available defenses. ​ 8\. Limitation of Liability TO THE FULLEST EXTENT PROVIDED BY LAW, IN NO EVENT SHALL OPENPHONE BE LIABLE FOR ANY LOSS OF PROFITS, REVENUE OR DATA, INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES, OR DAMAGES OR COSTS DUE TO BUSINESS INTERRUPTION, IN EACH CASE WHETHER OR NOT OPENPHONE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT, THE SERVICE, THE API, OPENPHONE DATA OR ANY COMMUNICATIONS, INTERACTIONS OR MEETINGS WITH OTHER USERS OF THE SERVICE OR THIRD PARTIES, ON ANY THEORY OF LIABILITY, INCLUDING TO THE EXTENT RESULTING FROM: (I) THE USE OR INABILITY TO USE THE SERVICE OR API; (II) ANY OTHER MATTER RELATED TO THE SERVICE OR API OR OPENPHONE DATA, WHETHER BASED ON WARRANTY, COPYRIGHT, CONTRACT, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER LEGAL THEORY, OR (III) FOR ANY AMOUNT EXCEEDING THE GREATER OF (X) THE AMOUNT OF FEES PAID TO OPENPHONE DURING THE TWELVE MONTHS IMMEDIATELY PRECEDING THE CLAIM OR (Y) \$100 (ONE HUNDRED DOLLARS). NOTWITHSTANDING THE FOREGOING, THE LIMITATIONS SET FORTH IN THIS SECTION 8 SHALL NOT LIMIT A PARTY’S LIABILITY UNDER SECTION 5 (CONFIDENTIALITY) OR SECTION 7 (INDEMNIFICATION). OPENPHONE ASSUMES NO RESPONSIBILITY FOR THE TIMELINESS, DELETION, MIS-DELIVERY OR FAILURE TO STORE ANY END USER CONTENT. ​ 9\. Miscellaneous 9.1 Assignment. Developer may not assign this Agreement without the prior written consent of OpenPhone. Subject to the foregoing limitation, this Agreement is binding upon and inures to the benefit of the successors and assigns of the respective parties hereto. 9.2 Independent Contractors. The relationship of the parties hereto is that of independent contractors. The parties hereto are not deemed to be agents, partners or joint ventures of the others for any purpose as a result of this Agreement or the transactions contemplated thereby. Nothing herein shall be deemed or construed as granting to either party or any right or authority to assume or to create any obligation or responsibility, express or implied, for, on behalf of, or in the name of the other party. All financial and other obligations associated with each party’s business are the sole responsibility of such party. 9.3 Third Party Beneficiaries. This Agreement is not intended and shall not be construed to create any rights or benefits upon any person not a party to this Agreement. 9.4 Force Majeure. Neither party shall be liable to the other in any way whatsoever for any failure or delay in performance of any of the obligations under this Agreement (other than obligations to make payment), arising out of any event or circumstance beyond the reasonable control of such party (including war, rebellion, civil commotion, terror, strikes, lock-outs or industrial disputes; fire, explosion, earthquake, acts of God, flood, drought or bad weather; acts of terror; epidemics, pandemics, or quarantine restrictions; or order by any government department, council or other constituted body). 9.5 Costs and Expenses. Unless specifically provided for elsewhere in this Agreement, each party shall bear its own costs and expenses, including legal fees, accounting fees and taxes incurred in connection with the negotiation and performance of this Agreement. 9.6 Compliance with Law. Developer shall at all times comply with all applicable international, federal, state and local laws and shall not engage in any illegal or unethical practices. Without limiting any of the foregoing, Developer agrees that it shall not permit the use of the Service or API or OpenPhone Data, export, or re-export the Service or API or OpenPhone Data, (a) into, or to or for the benefit of a national or resident of, any country to which the United States has embargoed goods, or (b) to anyone on the United States Treasury Department’s list of Specially Designated Nationals or the U.S. Commerce Department’s Table of Denial Orders, or license or otherwise permit use of the Service or API or OpenPhone Data for any activities involving nuclear materials or weapons, missile or rocket technologies, proliferation of chemical or biological weapons, or any other purpose prohibited by applicable law or in any jurisdiction where the Service is prohibited. 9.7 Notices. Except as otherwise provided, all notices under this Agreement shall be delivered by email, or physical mail to the other party at the address or number set forth in this Agreement. Notices to OpenPhone sent by physical mail shall also be sent via email to [support+developers@openphone.com](mailto:support+developers@openphone.com). Notices shall be deemed to have been given (i) at the time of delivery when delivered by email, (ii) at the time of delivery when delivered personally, or (iii) three (3) business days after having been sent by physical mail. 9.8 Entire Agreement; Modification. This Agreement, including any exhibits or other documents attached hereto or referenced herein, each of which is hereby incorporated into this Agreement and made an integral part hereof, constitutes the entire agreement between the parties relating to the subject matter hereof and there are no representations, warranties or commitments except as set forth herein. This Agreement supersedes all prior understandings, negotiations and discussions, written or oral, of the parties relating to the transactions contemplated by this Agreement. This Agreement may not be changed orally but only by an agreement in writing, signed by the party against whom enforcement of any waiver, change, modification, or discharge is sought. 9.9 Headings; Construction. The headings to the clauses, sub-clause and parts of this Agreement are inserted for convenience of reference only and are not intended to be part of or to affect the meaning or interpretation of this Agreement. The terms “this Agreement,” “hereof,” “hereunder” and any similar expressions refer to this Agreement and not to any particular Section or other portion of this Agreement. As used in this Agreement, the words “include” and “including,” and variations thereof, shall be deemed to be followed by the words “without limitation” and the word “discretion” means sole discretion. 9.10 Governing Law. This Agreement shall be governed by and construed in accordance with the laws of the State of California without giving effect to any conflict of law principles. The Federal and State courts located in San Francisco County, California shall be the exclusive venue for any disputes under this Agreement, and the parties hereby consent to the personal jurisdiction of those courts for such purposes. 9.11 Provisions Severable. If any provision of this Agreement shall be or become wholly or partially invalid, illegal or unenforceable, such provision shall be enforced to the extent that it is legal and valid and the validity, legality and enforceability of the remaining provisions shall in no way be affected or impaired. This Agreement shall be binding upon and inure to the benefit of the parties hereto and their respective successors, legal representatives and permitted assigns. 9.12 Waivers; Cumulative Remedies. No failure or delay by a party to insist upon the strict performance of any term or condition under this Agreement or to exercise any right or remedy available under this Agreement at law or in equity, shall imply or otherwise constitute a waiver of such right or remedy, and no single or partial exercise of any right or remedy by any party shall preclude exercise of any other right or remedy. All rights and remedies provided in this Agreement are cumulative and not alternative; and are in addition to all other available remedies at law or in equity. 9.13 Counterparts. This Agreement may be executed in two or more counterparts, each of which together shall be deemed an original, but all of which shall constitute one and the same instrument.