Overview
Webhooks provide real-time notifications when events occur in your Quo workspace, enabling powerful integrations and automated workflows. Configure webhooks to send event data to your applications instantly when calls complete, messages arrive, or contacts change.
How webhooks work
Event-driven notifications:
Webhooks deliver instant notifications when specific events occur in your Quo workspace. When an event triggers, Quo sends an HTTP POST request to your specified URL with detailed event data.
Typical workflow:
- Configure webhook URL and select event types
- Quo monitors for specified events
- When event occurs, Quo sends POST request with event payload
- Your application processes the event data and responds
- Quo logs the delivery status and retries if needed
Webhook configuration requires workspace Owner or Admin permissions. Settings are managed through web and desktop apps only.
Webhook configuration interface:
Available webhook events
Messaging events
Text message notifications:
message.received
: Text message received by workspace phone number (includes media attachments)
message.delivered
: Text message sent from workspace and successfully delivered (includes media)
AI-powered insights:
call.summary.completed
: AI-generated call summary available in event payload
call.transcript.completed
: Complete call transcript available in event payload
Voice events
Call status notifications:
call.ringing
: Incoming call being received by workspace phone number
call.completed
: Call finished (answered or unanswered, may include voicemail)
call.recording.completed
: Call recording available at provided URL
Contact management:
contact.updated
: Contact created or modified in workspace
contact.deleted
: Contact removed from workspace
Setting up webhooks
Configuration requirements
Required parameters:
Parameter | Requirement | Description |
---|
URL | Required | Webhook handler endpoint (HTTPS strongly recommended for production) |
Event types | Required | Select one or more event types to monitor |
Resources | Required | Choose phone numbers (calls/messages) or users/groups (contacts) to monitor |
Optional parameters:
Parameter | Description |
---|
Label | Descriptive name for webhook identification and management |
Setup process
To create a webhook:
- Navigate to Settings → Webhooks in Quo
- Click Create webhook
- Enter your webhook handler URL
- Select event types to monitor
- Choose phone numbers or contact resources
- Add optional label for identification
- Save and test configuration
Building webhook handlers
Handler requirements
Technical specifications:
- Accept HTTP POST requests at your webhook URL
- Process JSON event payload in request body
- Respond with 2xx HTTP status code within 10 seconds
- Verify webhook signature for security
- Handle retries and failures gracefully
Response handling:
- Success: Return 2xx status code (no response body required)
- Failure: Non-2xx response triggers Quo retry sequence
- Timeout: No response within 10 seconds initiates retries
Development flexibility:
Webhook handlers can be built in any programming language that supports HTTP requests and responses. Deploy to cloud platforms, servers, or serverless functions.
Security and authentication
Webhook signature verification:
All webhook calls include cryptographic signatures to verify authenticity and prevent spoofing attacks.
Signature header format:
'openphone-signature': 'hmac;1;1639710054089;mw1K4fvh5m9XzsGon4C5N3KvL0bkmPZSAy
b/9Vms2Qo='
Signature structure:
<scheme>;<version>;<timestamp>;<signature>
Component | Description | Current Value |
---|
scheme | Signature algorithm | Always “hmac” |
version | Signature version | Always “1” |
timestamp | Signature generation time | Unix timestamp |
signature | Base64 encoded HMAC signature | SHA256 digest |
Signature verification process
Verification steps:
- Extract components from
openphone-signature
header
- Prepare signed data by concatenating
timestamp + "." + payload
- Decode signing key from base64 (available in webhook details)
- Compute HMAC-SHA256 using decoded key and signed data
- Compare result with signature from header
Important requirements:
- Remove all whitespace and newlines from JSON payload before concatenation
- Use binary form of base64-decoded signing key for HMAC computation
- Ensure exact string matching for verification success
Getting your signing key:
- Go to webhook details page in Quo
- Click ellipses (⋯) at top right
- Select “Reveal signing secret”
- Copy base64-encoded key for your application
Implementation examples
Node.js webhook handler:
const express = require("express")
const bodyParser = require('body-parser')
const crypto = require('node:crypto');
const app = express()
const port = 8000
app.use(bodyParser.json())
app.post("/", function (req, res) {
// signingKey is from "Reveal Signing Secret" in the Quo app.
const signingKey = 'R2ZLM2o0bFhBNVpyUnU2NG9mYXQ1MHNyR3pvSUhIVVg='
// Parse the fields from the openphone-signature header.
const signature = req.headers['openphone-signature']
const fields = signature.split(';')
const timestamp = fields[2]
const providedDigest = fields[3]
// Compute the data covered by the signature.
const signedData = timestamp + '.' + JSON.stringify(req.body)
// Convert the base64-encoded signing key to binary.
const signingKeyBinary = Buffer.from(signingKey, 'base64').toString('binary')
// Compute the SHA256 HMAC digest.
const computedDigest = crypto.createHmac('sha256',signingKeyBinary)
.update(Buffer.from(signedData,'utf8'))
.digest('base64')
// Verify signature matches
if (providedDigest === computedDigest) {
console.log(`signature verification succeeded`)
// Process webhook event here
} else {
console.log(`signature verification failed`)
return res.status(401).send('Unauthorized')
}
res.send({})
});
app.listen(port, function () {
console.log(`webhook server listening on port ${port}`)
})
Python webhook handler:
import base64
import hmac
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/', methods=['POST'])
def handle_webhook_call():
# signingKey is from "Reveal Signing Secret" in the Quo app.
signing_key = 'R2ZLM2o0bFhBNVpyUnU2NG9mYXQ1MHNyR3pvSUhIVVg='
# Parse the fields from the openphone-signature header.
signature = request.headers['openphone-signature']
fields = signature.split(';')
timestamp = fields[2]
provided_digest = fields[3]
# Compute the data covered by the signature as bytes.
signed_data_bytes = b''.join([timestamp.encode(), b'.', request.data])
# Convert the base64-encoded signing key to bytes.
signing_key_bytes = base64.b64decode(signing_key)
# Compute the SHA256 HMAC digest.
hmac_object = hmac.new(signing_key_bytes, signed_data_bytes, 'sha256')
computed_digest = base64.b64encode(hmac_object.digest()).decode()
# Verify signature matches
if provided_digest == computed_digest:
print('signature verification succeeded')
# Process webhook event here
else:
print('signature verification failed')
return jsonify({'error': 'Unauthorized'}), 401
return jsonify({})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=True)
Future versions may include multiple signatures separated by commas. Split the header value on commas to handle multiple signatures if needed.
Security best practices
Replay attack protection:
Implement timestamp validation to prevent replay attacks:
- Compare signature timestamp with current time
- Reject requests with timestamps outside acceptable tolerance (e.g., 5 minutes)
- Each webhook call generates unique timestamp and signature
- Retries automatically include new timestamps
Additional security measures:
- Always use HTTPS URLs for production webhooks
- Store signing keys securely (environment variables, secret management)
- Implement proper error handling and logging
- Consider rate limiting for webhook endpoints
Error handling and retries
Automatic retry system
Retry behavior:
- Trigger conditions: Non-2xx response codes or 10-second timeout
- Backoff strategy: Exponential backoff with increasing delays
- Retry duration: Up to 3 days of retry attempts
- Final failure: Email notification sent to webhook creator
Retry timeline:
- Initial retries happen quickly for minimal delay
- Delays increase exponentially with each attempt
- Prioritizes delivery close to original event time
- Automatic status tracking throughout process
Manual retry options
Webhook management:
- View delivery status in Quo webhook details
- Manually retry failed webhook calls anytime
- Failed webhooks marked with ‘failure’ status
- Successful manual retries update status to ‘success’
Failure notifications
When retries exhaust:
- Email alert sent to webhook creator
- Webhook call marked as permanently failed
- Event details preserved for manual review
- Option to retry manually when issues resolved
Testing and validation
Development testing
Local testing options:
- HTTP clients: Use cURL, Postman, or Insomnia to send test POST requests
- Local URLs: Test against localhost during development
- Mock payloads: Create sample event JSON matching Quo format
- Signature testing: Verify HMAC validation logic with test keys
Quo test features
Built-in test request:
- Navigate to webhook details page in Quo
- Click ellipses (⋯) at top right
- Select “Send Test Request”
- Quo sends sample event to your webhook URL
- Verify signature verification and response handling
Test requests require publicly accessible webhook URLs. Local development URLs won’t work with Quo’s test feature.
Live event testing
Real event triggers:
- Configure webhook for specific event type (e.g.,
message.received
)
- Select your Quo number in webhook resources
- Trigger actual event (send text to your number)
- Monitor webhook delivery and processing
- Verify complete end-to-end functionality
Testing checklist:
- [ ] Webhook receives POST requests correctly
- [ ] Signature verification works
- [ ] Event payload parsing succeeds
- [ ] Application logic processes events properly
- [ ] Error responses trigger retries
- [ ] Success responses stop retry sequence
Troubleshooting
Common issues and solutions
Configuration problems:
- Verify webhook URL is correct and accessible
- Check webhook status is enabled in settings
- Confirm event types are properly selected
- Validate resources (phone numbers/users/groups) are correct
Response issues:
- HTTP status codes: Ensure 2xx responses for successful processing
- Response timing: Return responses within 10-second timeout
- Error handling: Implement proper error responses for debugging
Security verification:
- Signature validation: Verify HMAC signature computation
- Key format: Ensure signing key is properly base64 decoded
- Timestamp handling: Check timestamp extraction and concatenation
Event monitoring:
- Events log: View delivery history in webhook details page
- Status tracking: Monitor success/failure rates
- Retry attempts: Review automatic retry sequences
- Manual testing: Use “Send Test Request” for immediate validation
Performance optimization:
- Response time: Keep processing under 10 seconds
- Error rates: Minimize failures to reduce retry overhead
- Logging: Implement comprehensive request/response logging
- Monitoring: Set up alerts for webhook delivery failures
Event payload examples
Message events
message.received
payload:
{
"id": "EVc67ec998b35c41d388af50799aeeba3e",
"object": "event",
"apiVersion": "v2",
"createdAt": "2022-01-23T16:55:52.557Z",
"type": "message.received",
"data": {
"object": {
"id": "AC24a8b8321c4f4cf2be110f4250793d51",
"object": "message",
"from": "+14155550100",
"to": "+13105550199",
"direction": "incoming",
"body": "Hello",
"media": [
{
"url": "https://storage.googleapis.com/opstatics-dev/6c908000ada94d9fb206649ecb8cc928",
"type": "image/jpeg"
}
],
"status": "received",
"createdAt": "2022-01-23T16:55:52.420Z",
"userId": "USu5AsEHuQ",
"phoneNumberId": "PNtoDbDhuz",
"conversationId": "CN78ba0373683c48fd8fd96bc836c51f79"
}
}
}
message.delivered
payload:
{
"id": "EVdefd85c2c3b740429cf28ade5b69bcba",
"object": "event",
"apiVersion": "v2",
"createdAt": "2022-01-23T17:05:56.220Z",
"type": "message.delivered",
"data": {
"object": {
"id": "ACcdcc2668c4134c3cbfdacb9e273cac6f",
"object": "message",
"from": "+13105550199",
"to": "+14155550100",
"direction": "outgoing",
"body": "Have a nice day",
"media": [
{
"url": "https://opstatics-dev.s3.amazonaws.com/i/ab6084db-5259-42c0-93c1-e17fb2628567.jpeg",
"type": "image/jpeg"
}
],
"status": "delivered",
"createdAt": "2022-01-23T17:05:45.195Z",
"userId": "USu5AsEHuQ",
"phoneNumberId": "PNtoDbDhuz",
"conversationId": "CN78ba0373683c48fd8fd96bc836c51f79"
}
}
}
Call events
call.ringing
payload:
{
"id": "EV95c3708f9112412a834cc8d415470cd8",
"object": "event",
"apiVersion": "v2",
"createdAt": "2022-01-23T17:07:51.454Z",
"type": "call.ringing",
"data": {
"object": {
"id": "ACbaee66e137f0467dbed5ad4bc8d60800",
"object": "call",
"from": "+14155550100",
"to": "+13105550199",
"direction": "incoming",
"media": [],
"voicemail": null,
"status": "ringing",
"createdAt": "2022-01-23T17:07:51.116Z",
"answeredAt": null,
"completedAt": null,
"userId": "USu5AsEHuQ",
"phoneNumberId": "PNtoDbDhuz",
"conversationId": "CN78ba0373683c48fd8fd96bc836c51f79"
}
}
}
call.completed
(incoming with voicemail):
{
"id": "EVd39d3c8d6f244d21a9131de4fc9350d0",
"object": "event",
"apiVersion": "v2",
"createdAt": "2022-01-24T19:22:25.427Z",
"type": "call.completed",
"data": {
"object": {
"id": "ACa29ee906a4e04312a6928427b1c21721",
"object": "call",
"from": "+14145550100",
"to": "+13105550199",
"direction": "incoming",
"media": [],
"voicemail": {
"url": "https://m.openph.one/static/85ad4740be6048e4a80efb268d347482.mp3",
"type": "audio/mpeg",
"duration": 7
},
"status": "completed",
"createdAt": "2022-01-24T19:21:59.545Z",
"answeredAt": null,
"completedAt": "2022-01-24T19:22:19.000Z",
"userId": "USu5AsEHuQ",
"phoneNumberId": "PNtoDbDhuz",
"conversationId": "CN78ba0373683c48fd8fd96bc836c51f79"
}
}
}
call.completed
(outgoing answered call):
{
"id": "EV348de11e4b134fa48017ac45a251dd3e",
"object": "event",
"apiVersion": "v2",
"createdAt": "2022-01-24T19:28:45.370Z",
"type": "call.completed",
"data": {
"object": {
"id": "AC7ab6f57e62924294925d0ea961de7dc5",
"object": "call",
"from": "+13105550199",
"to": "+14155550100",
"direction": "outgoing",
"media": [],
"voicemail": null,
"status": "completed",
"createdAt": "2022-01-24T19:28:33.892Z",
"answeredAt": "2022-01-24T19:28:42.000Z",
"completedAt": "2022-01-24T19:28:45.000Z",
"userId": "USu5AsEHuQ",
"phoneNumberId": "PNtoDbDhuz",
"conversationId": "CN78ba0373683c48fd8fd96bc836c51f79"
}
}
}
call.recording.completed
payload:
{
"id": "EVda6e196255814311aaac1983005fa2d9",
"object": "event",
"apiVersion": "v2",
"createdAt": "2022-01-24T19:30:55.400Z",
"type": "call.recording.completed",
"data": {
"object": {
"id": "AC0d3b9011efa041d78c864019ad9e948c",
"object": "call",
"from": "+14155550100",
"to": "+13105550199",
"direction": "incoming",
"media": [
{
"url": "https://storage.googleapis.com/opstatics-dev/b5f839bc72a24b33a7fc032f78777146.mp3",
"type": "audio/mpeg",
"duration": 7
}
],
"voicemail": null,
"status": "completed",
"createdAt": "2022-01-24T19:30:34.675Z",
"answeredAt": "2022-01-24T19:30:38.000Z",
"completedAt": "2022-01-24T19:30:48.000Z",
"userId": "USu5AsEHuQ",
"phoneNumberId": "PNtoDbDhuz",
"conversationId": "CN78ba0373683c48fd8fd96bc836c51f79"
}
}
}
contact.updated
and contact.deleted
payload:
{
"id": "EVe844e47e9fa4494d9acfa1144839ed94",
"object": "event",
"createdAt": "2022-01-24T19:44:09.579Z",
"apiVersion": "v2",
"type": "contact.updated",
"data": {
"object": {
"id": "CT61eeff33f3b14cfe6358cb52",
"object": "contact",
"firstName": "Jane",
"lastName": "Smith",
"company": "Comp Inc",
"role": "Agent",
"pictureUrl": null,
"fields": [
{
"name": "Phone",
"type": "phone-number",
"value": "+14155551212"
},
{
"name": "Email",
"type": "email",
"value": null
},
{
"name": "Prop1",
"type": "string",
"value": "Value12"
}
],
"notes": [
{
"text": "@USu5AsEHuQ mynote 😂",
"enrichment": {
"taggedIds": {
"groupIds": [],
"userIds": [
"USu5AsEHuQ"
],
"orgIds": []
},
"tokens": {
"USu5AsEHuQ": {
"token": "USu5AsEHuQ",
"replacement": "Tom Smith",
"type": "mention",
"locations": [
{
"startIndex": 1,
"endIndex": 11
}
]
}
}
},
"createdAt": "2022-01-24T19:35:38.323Z",
"updatedAt": "2022-01-24T19:35:38.323Z",
"userId": "USu5AsEHuQ"
}
],
"sharedWith": [
"USu5AsEHuQ"
],
"createdAt": "2022-01-24T19:35:38.318Z",
"updatedAt": "2022-01-24T19:44:09.565Z",
"userId": "USu5AsEHuQ"
}
}
}
AI analysis events
call.summary.completed
payload:
{
"id": "EVc86d16fed5314cf6bd7bf13f11c65fd2",
"object": "event",
"apiVersion": "v3",
"createdAt": "2024-09-05T15:30:24.213Z",
"type": "call.summary.completed",
"data": {
"object": {
"object": "callSummary",
"callId": "AC641569d25e0a4489ad807409d9bee3fd",
"status": "completed",
"summary": [
"this is your call summary."
],
"nextSteps": [
"these are your next steps."
]
}
}
}
call.transcript.completed
payload:
{
"id": "EV67e3564088bf4badb6593cd7db8f2832",
"object": "event",
"apiVersion": "v3",
"createdAt": "2024-09-05T15:30:18.629Z",
"type": "call.transcript.completed",
"data": {
"object": {
"object": "callTranscript",
"callId": "AC741569d25e0a4489ad804709d9bee3fc",
"createdAt": "2024-09-05T15:30:18.198Z",
"dialogue": [
{
"end": 73.57498,
"start": 0.03998,
"userId": "UStpTEr9a6",
"content": "hello world",
"identifier": "+12345678901"
},
{
"end": 1.52,
"start": 1.12,
"userId": "USiAYGldYE",
"content": "hi there",
"identifier": "+19876543210"
}
],
"duration": 87.614685,
"status": "completed"
}
}
}
Additional resources
Need help? Submit a support request at support.openphone.com for technical assistance with webhook implementation.