Guide to PubNub Webhooks: Features and Best Practices
PubNub webhooks let your backend hear about what happens on your realtime network: a message is published, a user joins or leaves a channel, an App Context user or channel changes, or a mobile push fails. If you build chat, live updates or IoT on PubNub, a Webhook action is how those events reach a service you run, for moderation, analytics, archiving or syncing state.
This guide covers how Events & Actions listeners and Webhook actions fit together, the event types and payload formats, how to authenticate requests with a secret header, PubNub's retry and batching rules, and the best practices for production.
What are PubNub webhooks?
PubNub delivers webhooks through Events & Actions. You build two things in the Admin Portal:
- An event listener chooses a source, such as messages or presence, and a filter for the events you want.
- A Webhook action sends each matching event to your HTTPS endpoint as a JSON request.
A listener can trigger one or more actions, depending on your plan. Webhook is one action type among several. Others send events to Amazon SQS, Kinesis, S3 or an AMQP exchange.
Events & Actions replaced the older webhooks configured on the Keysets page. PubNub migrated paid-plan keyset webhooks automatically before 16 January 2024, but the payload shape changed, so handlers written for legacy webhooks need updating. This guide covers Webhook actions only.
PubNub does not sign Webhook action requests. To authenticate them, you add custom headers to the action, with values taken from secret keys stored on your keyset, and your endpoint checks the header.
PubNub webhook features
| Feature | Details |
|---|---|
| Configuration | Admin Portal, Events & Actions: an event listener paired with a Webhook action |
| Transport | JSON over HTTPS |
| Event identifier | dataSchema, a schema identifier such as pubnub.com/schemas/events/messages.pubsub.publish?v=1.0.0 |
| Formats | No Envelope (default), or V1, V2 or V2.1 envelopes with Is Enveloped turned on |
| Authentication | Custom headers with values from keyset secret keys. No signature |
| Batching | Optional. Sends when 100 events (1 to 10,000) or 5 seconds (1 to 300) is reached, whichever is first |
| Success response | Any 2xx. A 301 is followed for up to three redirects |
| Retries | Paid tiers only: 1 to 4 retries (default 2), jittered backoff from a 10 to 900 second base (default 450) |
| Free tier | One listener, Webhook actions only, no retries |
| Source IPs | Not published |
Common events
PubNub's payloads page documents 22 event types. Message sent and File sent share one schema identifier, so there are 21 distinct dataSchema values. The table shows the part between pubnub.com/schemas/events/ and ?v=.
| Area | Event types |
|---|---|
| Messages | messages.pubsub.publish (message and file sent), messages.action.created, messages.action.deleted |
| Presence | presence.user.channel.joined, presence.user.channel.left, presence.user.timedout.in.channel, presence.channel.state.active, presence.channel.state.inactive, presence.channel.occupancy.counted, presence.channel.user.state.in.changed |
| App Context | user.created, user.updated, user.deleted, channel.created, channel.updated, channel.deleted, membership.created, membership.updated, membership.deleted |
| Mobile push | push.device.removed, push.message.sending.failed |
The default No Envelope body has two fields: the schema identifier, and a data array of event records. Here is PubNub's Message sent example:
{
"dataSchema": "pubnub.com/schemas/events/messages.pubsub.publish?v=1.0.0",
"data": [
{
"timestamp": "2025-06-05T12:53:15.888Z",
"message": "Message Test",
"meta": null,
"senderId": "Jack-device",
"channel": "Channel-Barcelona",
"timetoken": "17491279958880737"
}
]
}
PubNub says the dataSchema and schema values "are identifiers, not URLs you can open" and should be compared as exact strings. Each one ends with a version (?v=1.0.0), so if you want routing that survives a version change, strip the prefix and version and route on the dotted name.
With Is Enveloped turned on, the same event moves down a level. In the V2 and V2.1 envelopes (2.1 is the default), the event sits at event.payload beside a metadata object, and the top-level schema is the envelope's own identifier, not the event's. The V1 envelope uses schema instead of dataSchema for most event types, and Message sent and File sent use a different V1 layout again. Pick one format per action and parse only that one.
Setting up PubNub webhooks
- Generate a long random value, for example with
openssl rand -hex 32, and store it as a secret key on your keyset. - In the Admin Portal, open Events & Actions, click + Add Action, and choose Webhook.
- Under Configuration, enter your HTTPS endpoint URL.
- Add a custom header, such as
X-PubNub-Secret, and select the secret key as its value. - Decide whether you want Batching, Webhook retry and Is Enveloped. Turn on Webhook retry if your plan allows it.
- Click Add event listener to pair the action with an existing listener, or create one with the source and filter you need, then Save changes.
- Publish a test message that matches the listener's filter and check that your endpoint receives it.
In Hookdeck. Create a source with the PubNub type. Enter the custom header's name in Header Name and the secret key's exact value in Header Value, and use the source URL as the action's endpoint. Hookdeck matches the header name case-insensitively and the value exactly. The source type covers Events & Actions Webhook actions only, because legacy keyset webhooks can't send a secret header.
Securing PubNub webhooks
Compare the custom header with the secret value in constant time, and reject the request if the header is missing or different.
const crypto = require('crypto');
const express = require('express');
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on buffers of different lengths
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const SCHEMA = /^pubnub\.com\/schemas\/events\/([^?]+)\?v=/;
const app = express();
app.post('/webhooks/pubnub', express.raw({ type: '*/*' }), (req, res) => {
const expected = process.env.PUBNUB_WEBHOOK_SECRET;
if (!expected) return res.sendStatus(500); // fail closed when unconfigured
const received = req.get(process.env.PUBNUB_WEBHOOK_HEADER || 'X-PubNub-Secret');
if (typeof received !== 'string' || !safeEqual(received, expected)) {
return res.sendStatus(401);
}
let body;
try {
body = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// No Envelope format: { dataSchema, data: [...] }
const type = SCHEMA.exec(body.dataSchema || '')?.[1]; // e.g. "messages.pubsub.publish"
for (const record of body.data ?? []) {
// queue { type, record } for a background worker
}
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
def verify_pubnub_request(headers) -> bool:
expected = os.environ.get("PUBNUB_WEBHOOK_SECRET")
if not expected:
return False # fail closed
header_name = os.environ.get("PUBNUB_WEBHOOK_HEADER", "X-PubNub-Secret")
received = headers.get(header_name) # Starlette and Flask headers are case-insensitive
if not received:
return False
return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))
A matching header proves that the sender knows the secret. It doesn't prove that the body is unchanged, and a captured request can be replayed later and still pass, because PubNub sends no signature and no timestamp. Keep the endpoint on HTTPS, use a long random secret, and rotate it on the keyset and in your configuration if it may have leaked.
Make PubNub webhooks production-ready. Hookdeck Event Gateway checks the secret header on every request, stores each event durably, and retries delivery to your service on any plan.
PubNub webhook limitations and pain points
No retries on the Free tier
The Problem: On the Free tier, any failed delivery is lost. On paid tiers, the default is only two retries.
Why It Happens: Events & Actions retries are a paid feature. Even with them on, an action retries 1 to 4 times, with a random delay of at least 10 seconds and at most 900 seconds per attempt, so a long outage still loses events.
Workarounds:
- Turn on Webhook retry and raise the retry count to 4 if your plan allows it.
- Keep your endpoint fast and highly available, and answer
2xxas soon as the header checks out. - For data you can't lose, such as chat messages, also enable Message Persistence and backfill from history after an outage.
How Hookdeck Can Help: Hookdeck accepts each request as soon as it arrives and stores it, then delivers it to your service with retries on a schedule you control, on any PubNub tier. You can replay any event after you fix a bug.
Several payload shapes
The Problem: A handler written against one example fails on real traffic, or a migrated legacy handler reads fields that no longer exist.
Why It Happens: The same event can arrive in four formats (No Envelope, V1, V2 and V2.1), batching changes how many events a request holds, and the legacy keyset webhooks used another shape again. PubNub's payloads page doesn't show the batched body.
Workarounds:
- Choose one format per action and keep it. No Envelope is the simplest.
- Always iterate over
data, even when you expect one record. - Send a test event after any configuration change and look at the actual body before deploying.
How Hookdeck Can Help: Every request that reaches a Hookdeck source is stored with its full body and headers, so you can see the exact shape your action sends and replay it against your handler while you write the parser.
A secret header, not a signature
The Problem: Anyone who learns the header value can post fake messages or presence events to your endpoint.
Why It Happens: Webhook actions don't sign requests. The custom header is the same static value on every request.
Workarounds:
- Use a long random secret, HTTPS only, and keep it out of logs and client code.
- Deduplicate on fields that identify the event, such as
timetokenandchannelfor messages. - Before acting on something sensitive, confirm it through the PubNub API.
How Hookdeck Can Help: Hookdeck's PubNub source type compares the header you name with the exact value you enter. Hookdeck answers PubNub with a 200 either way, then records a request without the right header as failed verification, so it never reaches your destination.
Presence and message volume
The Problem: Busy channels produce far more requests than your endpoint was sized for.
Why It Happens: Every published message or presence change that matches a listener's filter produces an event. Without batching, each one is its own request.
Workarounds:
- Narrow listener filters to the channels and events you act on.
- Turn on batching for high-volume listeners, and size the batch for your endpoint's request limits.
How Hookdeck Can Help: Hookdeck absorbs bursts at the edge and can rate-limit delivery to your service, so a busy channel can't overwhelm it. Filters can route message and presence events to different destinations.
Best practices
Route on dataSchema as an exact string
Compare schema identifiers exactly, as PubNub recommends, or strip the prefix and version and route on the dotted name. Don't try to fetch them as URLs.
Iterate every record
data is an array. Handle each record, and make sure one bad record doesn't fail the whole request, because a failed request is retried in full on paid tiers.
Deduplicate deliveries
Retries resend events you may already have processed. For messages, timetoken with channel is a good key. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Return 2xx as soon as the header checks out, then process in a background job. See why to process webhooks asynchronously.
Keep the secret server-side
The header value comes from a keyset secret key. Keep it in your server's environment and never in code that ships to browsers or devices.
Conclusion
PubNub forwards realtime events through Events & Actions Webhook actions, with a custom header from your keyset as the only credential. Compare that header in constant time over HTTPS, route on dataSchema, iterate every record in data, pick one payload format per action, and plan for the limited retries, or none at all on the Free tier.
Hookdeck Event Gateway sits in front of your receiver, checks the secret header, absorbs bursts, and keeps every event until your service has processed it.
Get started with Hookdeck for free and handle PubNub webhooks reliably in minutes.