Agent skill

PubNub Webhooks Skill

Receive and authenticate PubNub Events & Actions webhooks. Use when setting up a PubNub Webhook action endpoint, verifying the secret custom header, debugging 401s, or handling events like messages.pubsub.publish, user.created, channel.created, membership.created and presence.user.channel.joined.

Install this skill

npx skills add hookdeck/webhook-skills --skill pubnub-webhooks


PubNub's Events & Actions can forward events (published messages, presence changes, user, channel and membership metadata changes, push failures) to your HTTPS endpoint with a Webhook action. Webhooks are not signed. If your endpoint needs authentication, you add a secret custom header to the action (its value comes from a keyset secret), and your endpoint checks that header on every request. This skill covers the Events & Actions Webhook action; legacy webhooks configured on the Keysets page are not covered.

When to Use This Skill

  • How do I receive PubNub webhooks?
  • Does PubNub sign webhook payloads? (No: use a secret custom header)
  • How do I authenticate requests from a PubNub Webhook action?
  • How do I handle messages.pubsub.publish, user.created or presence.user.channel.joined events?
  • Why does my PubNub webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyPubNubRequest(headers, secret = process.env.PUBNUB_WEBHOOK_SECRET,
                             name = process.env.PUBNUB_WEBHOOK_HEADER || 'Authorization') {
  if (!secret) return false;                  // never accept when unconfigured
  const received = headers[name.toLowerCase()]; // Node lowercases header names
  return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}
  • The header name is whatever you typed into the action (PUBNUB_WEBHOOK_HEADER, default Authorization). Look it up case-insensitively.
  • The expected value is the whole header value (PUBNUB_WEBHOOK_SECRET), including any prefix you typed such as Bearer . Compare it as is; don't strip or parse a prefix.
  • Return 401 for a missing header or a mismatch, 400 for a body that isn't a No Envelope payload, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) so any content type works, and authenticate before parsing it. In Python use hmac.compare_digest on bytes (.encode() both sides).

A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Serve the endpoint over HTTPS, keep the secret out of logs, and rotate it if it leaks. Dedupe on the event id or timetoken if replays matter.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

With Is Enveloped off, the body is { "dataSchema": "pubnub.com/schemas/events/<type>?v=1.0.0", "data": [ ... ] }. Strip the pubnub.com/schemas/events/ prefix and the ?v=... suffix to get the event type:

Event type (dataSchema)Sent When
messages.pubsub.publishA message is published, or a file is sent (file items carry fileId, fileName)
messages.action.created / messages.action.deletedA message action is added or removed
presence.user.channel.joined / presence.user.channel.leftA user subscribes to or unsubscribes from a channel
presence.user.timedout.in.channelA user's subscription drops without an unsubscribe
presence.channel.user.state.in.changedA user's presence state in a channel changes
presence.channel.state.active / presence.channel.state.inactiveFirst user subscribes / last user leaves a channel
presence.channel.occupancy.countedA channel reports occupancy at its interval
user.created / user.updated / user.deletedUser metadata is set, changed or removed
channel.created / channel.updated / channel.deletedChannel metadata is set, changed or removed
membership.created / membership.updated / membership.deletedMembership metadata is set, changed or removed
push.device.removed / push.message.sending.failedA push device token is removed / a push attempt fails

Environment Variables

PUBNUB_WEBHOOK_HEADER=Authorization     # name of the custom header you added to the action
PUBNUB_WEBHOOK_SECRET=your-shared-secret # exact whole header value, e.g. "Bearer abc123"

Local Development

PubNub can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 pubnub --path /webhooks/pubnub

Use the printed URL as the endpoint URL in the Webhook action.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →