Webhooks
How you can get notified of events that happen in Whippy.
Configuration
Whippy can be configured to send webhooks as POST requests for various events. This can be set up through the Developers tab in Settings. For each API key you can create many Developer Applications. An application can be associated with only one key. After you create an application, you can add Endpoints to it. These are endpoints of servers that would receive webhooks. You can have one that receives all webhooks or many for different events. Each developer endpoint is subscribed to different events and would receive webhooks for them. Developer applications and endpoints can be enabled and disabled by the users. In order to receive webhooks, both the developer application and the specific endpoint that the will be sent to need to be enabled.
Events
Whippy webhook events have a format that consists of an event resource and an event type joined by a dot. For example, for a newly created message, an event named "message.created"
will be sent, while for an updated campaign "campaign.updated"
will be sent. You don't have to receive webhooks for all events. When you create a developer endpoint, you choose which events you want to subscribed to. An endpoint can subscribe to many events, and an event can be added to many endpoints.
Webhook Body
The body of the webhook POST requests has the following format:
{
"data": { ... },
"event": "event.type",
"request_id": "unique.id"
}
For example, for a new message the webhook sent would will have the following payload:
{
"data":{
"id":"5e09c7cc-27e8-4d91-89ee-4ea5bafaaa15",
"to":"+14244023931",
"body":"The answer to the Great Question is 42.",
"from":"+12183925232",
"step_id":null,
"language":null,
"direction":"OUTBOUND",
"channel_id":"be1ecaf2-9ce7-448c-8755-e5009ccc4345",
"contact_id":"8241b8e5-98a2-44d7-975b-15f5c2bf3cf4",
"created_at":"2023-08-24T10:05:18.432652Z",
"updated_at":"2023-08-24T10:05:18.432652Z",
"attachments":[
],
"campaign_id":null,
"conversation_id":"742993a5-c9c6-4d7d-bf09-31417a1c04a9",
"delivery_status":"pending",
"step_contact_id":null,
"translated_body":null,
"campaign_contact_id":null,
"translation_language":null
},
"event":"message.created",
"request_id":"00552b38-d0e7-460f-9877-a7c8c8b2ff8c"
}
You can see a list of all webhook events with examples in the webhook examples section.
Retrying
If a webhook is not successful, it will be retried after a period of time. It will be retried 6 times with an increasing amount of time between them where the last attempt could happen on the following day. This is to ensure any issues on the receiving end have a higher chance of being resolved.
Whippy saves each webhook request and response so that you can at any time follow if the webhooks are processed as expected.
Ordering
Whippy webhooks are processed in the order they are created at, as they are all ordered based on our internal timestamps. This is important for message events, as creating and sending a message will result in approximately 4 events, one of type message.created
and 3 of type message.updated
. We want to ensure that the webhooks for these events are received in the same order they occur. However, if another event, for example campaign.created
, happens around the same time as a message.created
event, then there is no guarantee that these events will come in the correct order. This can happen due to concurrency and the fact that the processing time for webhooks of different resources may vary, e.g. a message.created
event can start being processed right after a campaign.created
event, and finish while campaign.created
is still being processed.
Verification
Each Whippy webhook request contains the X-Whippy-Signature
custom header. It contains a value that can be used to verify that the webhook is sent by Whippy. It is generated in the following manner:
- for each webhook attempt, a UNIX timestamp is generated;
- the body of the webhook is encoded in JSON without being prettified;
- the timestamp and the encoded body are added to a string with a dot between them, e.g.
"unix_timestamp.encoded_body"
; - the string is encrypted by using a hash-based message authentication code (HMAC) with a SHA-256 cryptographic hash function using the API key associated with the developer application that the webhook is part of (each webhook is associated with the developer endpoint that it is sent to and each endpoint belongs to a developer application);
- it is then Base64-encoded without padding;
- in the end it is sent as the value for the
X-Whippy-Signature
together with the UNIX timestamp in the format of:"t=#{unix_timestamp},v1=#{signed_payload}"
.v1
stands for the signature scheme version which is currently 1, there are no other signature schemes as of now; - users can extract the timestamp and use their API key and webhook body to verify that the request is valid;
You can verify the signature in the following manner, for example, in JavaScript, Python, and Ruby.
The examples below use a real webhook received in https://webhook.site, an application useful for testing webhooks.
JavaScript
#!/usr/bin/env node
const crypto = require('crypto');
// Configuration
// Get the API key associated with the Webhook - it is set when creating a Developer Application and Endpoints
const apiKey = "ebb671d8-ce00-4c71-a6a5-d2cf2850ba97";
// Get the X-Whippy-Signature header from the webhook
const whippySignatureHeader = "t=1756814608,v1=5Owa2+Za3nZYoUJ5JCaK41PyytfmJ/S9pFCYegguwhY";
// Get the full JSON body of the webhook
const jsonPayload = '{"api_version":"v1","created_at":"2025-09-02T12:03:28.185059Z","data":{"attachments":[],"body":"This is a cool message!","campaign_contact_id":null,"campaign_id":null,"channel_id":"4051c97b-7f37-45b9-99f5-06639aa15427","contact_id":"a4e02ae3-e098-40fc-91f2-75e809c04d20","conversation_id":"603e402f-2ca7-44a1-b4f4-e99ab2159efd","created_at":"2025-09-02T12:03:21.757377Z","delivery_status":"pending","direction":"OUTBOUND","from":"+14244023929","id":"198e39ea-7bd8-4015-98c1-2466e8dfe567","language":null,"metadata":{},"mode":"manual","sequence_id":null,"step_contact_id":null,"step_id":null,"to":"+15615557689","translated_body":null,"translation_language":null,"type":"sms","updated_at":"2025-09-02T12:03:21.757377Z","user_id":1},"event":"message.created","request_id":"b517a515-1e30-4e70-aa67-828abc28706f"}';
// Parse the signature header
const [timestampPart, signaturePart] = whippySignatureHeader.split(",");
// Extract the Unix timestamp
const unixTimestamp = timestampPart.replace("t=", "");
console.log(`Unix timestamp: ${unixTimestamp}`);
// Extract the hashed signature
const expectedSignature = signaturePart.replace("v1=", "");
console.log(`Expected signature: ${expectedSignature}`);
// Create a payload for HMAC verification
const payload = `${unixTimestamp}.${jsonPayload}`;
console.log(`Payload length: ${payload.length}`);
// Generate the HMAC signature
const hmac = crypto.createHmac('sha256', apiKey);
hmac.update(payload);
const computedSignature = hmac.digest('base64').replace(/=+$/, '');
console.log(`Computed signature: ${computedSignature}`);
// Verify the signature
if (computedSignature === expectedSignature) {
console.log("✅ Signature is VALID!");
} else {
console.log("❌ Signature is INVALID!");
console.log(`Expected: ${expectedSignature}`);
console.log(`Computed: ${computedSignature}`);
}
Python
#!/usr/bin/env python3
import hmac
import hashlib
import base64
# Configuration
# Get the API key associated with the Webhook - it is set when creating a Developer Application and Endpoints
api_key = "ebb671d8-ce00-4c71-a6a5-d2cf2850ba97"
# Get the X-Whippy-Signature header from the webhook
whippy_signature_header = "t=1756814608,v1=5Owa2+Za3nZYoUJ5JCaK41PyytfmJ/S9pFCYegguwhY"
# Get the full JSON body of the webhook
json_payload = '{"api_version":"v1","created_at":"2025-09-02T12:03:28.185059Z","data":{"attachments":[],"body":"This is a cool message!","campaign_contact_id":null,"campaign_id":null,"channel_id":"4051c97b-7f37-45b9-99f5-06639aa15427","contact_id":"a4e02ae3-e098-40fc-91f2-75e809c04d20","conversation_id":"603e402f-2ca7-44a1-b4f4-e99ab2159efd","created_at":"2025-09-02T12:03:21.757377Z","delivery_status":"pending","direction":"OUTBOUND","from":"+14244023929","id":"198e39ea-7bd8-4015-98c1-2466e8dfe567","language":null,"metadata":{},"mode":"manual","sequence_id":null,"step_contact_id":null,"step_id":null,"to":"+15615557689","translated_body":null,"translation_language":null,"type":"sms","updated_at":"2025-09-02T12:03:21.757377Z","user_id":1},"event":"message.created","request_id":"b517a515-1e30-4e70-aa67-828abc28706f"}';
# Parse the signature header
timestamp_part, signature_part = whippy_signature_header.split(",")
# Extract the Unix timestamp
unix_timestamp = timestamp_part.replace("t=", "")
print(f"Unix timestamp: {unix_timestamp}")
# Extract the signature
expected_signature = signature_part.replace("v1=", "")
print(f"Expected signature: {expected_signature}")
# Create a payload for HMAC verification
payload = f"{unix_timestamp}.{json_payload}"
print(f"Payload length: {len(payload)}")
# Generate the HMAC signature
computed_signature = base64.b64encode(
hmac.new(
api_key.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).digest()
).decode('utf-8').rstrip('=')
print(f"Computed signature: {computed_signature}")
# Verify the signature
if computed_signature == expected_signature:
print("✅ Signature is VALID!")
else:
print("❌ Signature is INVALID!")
print(f"Expected: {expected_signature}")
print(f"Computed: {computed_signature}")
Ruby
#!/usr/bin/env ruby
require 'openssl'
require 'base64'
# Configuration
# Get the API key associated with the Webhook - it is set when creating a Developer Application and Endpoints
api_key = "ebb671d8-ce00-4c71-a6a5-d2cf2850ba97"
# Get the X-Whippy-Signature header from the webhook
whippy_signature_header = "t=1756814608,v1=5Owa2+Za3nZYoUJ5JCaK41PyytfmJ/S9pFCYegguwhY"
# Get the full JSON body of the webhook
json_payload = '{"api_version":"v1","created_at":"2025-09-02T12:03:28.185059Z","data":{"attachments":[],"body":"This is a cool message!","campaign_contact_id":null,"campaign_id":null,"channel_id":"4051c97b-7f37-45b9-99f5-06639aa15427","contact_id":"a4e02ae3-e098-40fc-91f2-75e809c04d20","conversation_id":"603e402f-2ca7-44a1-b4f4-e99ab2159efd","created_at":"2025-09-02T12:03:21.757377Z","delivery_status":"pending","direction":"OUTBOUND","from":"+14244023929","id":"198e39ea-7bd8-4015-98c1-2466e8dfe567","language":null,"metadata":{},"mode":"manual","sequence_id":null,"step_contact_id":null,"step_id":null,"to":"+15615557689","translated_body":null,"translation_language":null,"type":"sms","updated_at":"2025-09-02T12:03:21.757377Z","user_id":1},"event":"message.created","request_id":"b517a515-1e30-4e70-aa67-828abc28706f"}'
# Parse the signature header
timestamp_part, signature_part = whippy_signature_header.split(",")
# Extract the Unix timestamp
unix_timestamp = timestamp_part.sub("t=", "")
puts "Unix timestamp: #{unix_timestamp}"
# Extract the signature
expected_signature = signature_part.sub("v1=", "")
puts "Expected signature: #{expected_signature}"
# Create a payload for HMAC verification
payload = "#{unix_timestamp}.#{json_payload}"
puts "Payload length: #{payload.length}"
# Generate the HMAC signature
computed_signature = Base64.encode64(
OpenSSL::HMAC.digest('sha256', api_key, payload)
).strip.delete("=")
puts "Computed signature: #{computed_signature}"
# Verify the signature
if computed_signature == expected_signature
puts "✅ Signature is VALID!"
else
puts "❌ Signature is INVALID!"
puts "Expected: #{expected_signature}"
puts "Computed: #{computed_signature}"
end
Testing
If you want to test receiving webhooks before integrating with your application, we recommend that using Pipe Dream’s Request Bin. You can create a public endpoint with a unique identifier and set it up as a Developer Endpoint in Whippy, and select which events you want the webhook url to subscribe to. Once you have this configured you can generate events by taking actions in Whippy e.g. sending a message, creating a contact or starting a campaign.
Helpful Resources
If you’re new to learning about webhooks or just need a refresher on how they work, here are some helpful resources that we have found useful for both technical and non technical audiences alike.
Updated 14 days ago