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. To receive webhooks, both the developer application and the specific endpoint to which they will be sent 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 subscribe to. An endpoint can subscribe to many events, and an event can be added to many endpoints.

Webhook Body

The body of each webhook POST request uses this envelope:

{
  "data": {},
  "api_version": "v1",
  "event": "event.type",
  "created_at": "2025-08-19T06:40:57.416929Z",
  "request_id": "unique.id"
}
FieldDescription
dataResource payload for the event (shape depends on the event type).
api_versionAPI version used to render the payload (currently "v1").
eventEvent name, for example "message.created".
created_atWhen Whippy attempted this delivery (ISO 8601 UTC). Used for signature verification.
request_idUnique id for this delivery attempt.

For example, a new message webhook looks like:

{
  "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",
    "type": "sms",
    "mode": "manual",
    "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,
    "sequence_id": null,
    "user_id": null,
    "error": null,
    "error_code": null,
    "error_message": null,
    "metadata": {},
    "email_metadata": {}
  },
  "api_version": "v1",
  "event": "message.created",
  "created_at": "2025-08-19T06:40:57.416929Z",
  "request_id": "00552b38-d0e7-460f-9877-a7c8c8b2ff8c"
}

Webhook resource pages are grouped by platform (same layout as the API Reference):

  • Communication — messages, calls, conversations, templates, and link clicks
  • Marketing — campaigns and forms
  • Automation — sequences, steps, step contacts, and sequence runs
  • Data — contacts, leads, preferences, and custom objects
  • Agent Platform — conversation agent sessions and tool executions

See also Webhook Filtering for inclusion filters.

Retrying

If a webhook request is not successful, it will be retried up to 2 more times (3 attempts total) with an increasing backoff period between them. This is to ensure any temporary issues on the receiving end have a higher chance of being resolved.

For a webhook to be considered successful, your endpoint must return a 2xx HTTP status code. Any other status code (e.g. 4xx, 5xx) is treated as a failure and will trigger a retry. A webhook attempt can also fail due to connection errors if your endpoint is unreachable, or a timeout if your endpoint does not respond within 10 seconds.

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, 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 signed with a hash-based message authentication code (HMAC) using a SHA-256 hash function and 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). HMAC authenticates the payload; it does not encrypt it;
  • 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. Replace YOUR_WHIPPY_API_KEY, the signature header, and the payload with values from a real webhook delivery — the placeholders below will not verify.

The examples below use a real webhook received in https://webhook.site, an application useful for testing webhooks.

X-Whippy-Signature header on a webhook request in webhook.site

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 = "YOUR_WHIPPY_API_KEY";
// Get the X-Whippy-Signature header from the webhook
const whippySignatureHeader = "t=1756814608,v1=BASE64_HMAC_SIGNATURE";
// 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 and reject stale deliveries (replay protection)
const unixTimestamp = Number(timestampPart.replace("t=", ""));
const toleranceSeconds = 300;
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - unixTimestamp);
if (!Number.isFinite(unixTimestamp) || ageSeconds > toleranceSeconds) {
  console.log("❌ Signature timestamp is outside the allowed tolerance.");
  process.exit(1);
}
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}`);

// Compare signatures in constant time
const expectedBuf = Buffer.from(expectedSignature);
const computedBuf = Buffer.from(computedSignature);
const signaturesMatch =
  expectedBuf.length === computedBuf.length &&
  crypto.timingSafeEqual(expectedBuf, computedBuf);

if (signaturesMatch) {
  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
import time

# Configuration
# Get the API key associated with the Webhook - it is set when creating a Developer Application and Endpoints
api_key = "YOUR_WHIPPY_API_KEY"
# Get the X-Whippy-Signature header from the webhook
whippy_signature_header = "t=1756814608,v1=BASE64_HMAC_SIGNATURE"
# 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 and reject stale deliveries (replay protection)
unix_timestamp = int(timestamp_part.replace("t=", ""))
tolerance_seconds = 300
if abs(int(time.time()) - unix_timestamp) > tolerance_seconds:
    raise SystemExit("Signature timestamp is outside the allowed tolerance.")
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}")

# Compare signatures in constant time
if hmac.compare_digest(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 = "YOUR_WHIPPY_API_KEY"
# Get the X-Whippy-Signature header from the webhook
whippy_signature_header = "t=1756814608,v1=BASE64_HMAC_SIGNATURE"
# 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 and reject stale deliveries (replay protection)
unix_timestamp = Integer(timestamp_part.sub("t=", ""))
tolerance_seconds = 300
if (Time.now.to_i - unix_timestamp).abs > tolerance_seconds
  abort "Signature timestamp is outside the allowed tolerance."
end
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}"

# Compare signatures in constant time
signatures_match =
  begin
    OpenSSL.fixed_length_secure_compare(computed_signature, expected_signature)
  rescue ArgumentError
    false
  end

if signatures_match
  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 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.



Did this page help you?