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.createdorpresence.user.channel.joinedevents? - 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, defaultAuthorization). Look it up case-insensitively. - The expected value is the whole header value (
PUBNUB_WEBHOOK_SECRET), including any prefix you typed such asBearer. Compare it as is; don't strip or parse a prefix. - Return
401for a missing header or a mismatch,400for a body that isn't a No Envelope payload,200once handled. - Read the body raw (
express.raw({ type: '*/*' })) so any content type works, and authenticate before parsing it. In Python usehmac.compare_digeston 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
idortimetokenif 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.publish | A message is published, or a file is sent (file items carry fileId, fileName) |
messages.action.created / messages.action.deleted | A message action is added or removed |
presence.user.channel.joined / presence.user.channel.left | A user subscribes to or unsubscribes from a channel |
presence.user.timedout.in.channel | A user's subscription drops without an unsubscribe |
presence.channel.user.state.in.changed | A user's presence state in a channel changes |
presence.channel.state.active / presence.channel.state.inactive | First user subscribes / last user leaves a channel |
presence.channel.occupancy.counted | A channel reports occupancy at its interval |
user.created / user.updated / user.deleted | User metadata is set, changed or removed |
channel.created / channel.updated / channel.deleted | Channel metadata is set, changed or removed |
membership.created / membership.updated / membership.deleted | Membership metadata is set, changed or removed |
push.device.removed / push.message.sending.failed | A 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
- references/overview.md - What Events & Actions webhooks are, events and payload shape
- references/setup.md - Create the Webhook action and add the secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests