Guide to Podium Webhooks: Features and Best Practices
Podium webhooks tell your systems what's happening with a business's customers on Podium: a message arrives or fails to send, a contact is created or merged, a review is posted, a call is missed, or an invoice is paid. Podium provides messaging, reviews and payments for local businesses, and webhooks let partners and in-house teams react to that activity as it happens.
This guide covers how Podium webhooks are created and signed, the event types, how Podium queues events, and the practices that keep a Podium receiver reliable.
What are Podium webhooks?
A Podium webhook belongs to exactly one location or organization and lists the event types it receives. You create webhooks only through the webhook API, with a token granted by a user of that location or organization. Each event type is tied to an API scope, so the token needs the scopes for every type you subscribe to.
The secret is optional when you create a webhook, and it decides whether deliveries are signed. Podium says "any webhook with no secret will not be signed". With a secret, each delivery carries podium-timestamp and podium-signature, an HMAC-SHA256 of the timestamp, a ., and the raw body.
Podium webhook features
| Feature | Details |
|---|---|
| Configuration | The webhook API resource only |
| Scope | Exactly one location or organization per webhook |
| Permissions | A token from a user of that location or organization, with the scopes for the event types |
| Signature header | podium-signature. 64 hex characters in Podium's screenshots; the encoding isn't stated in text |
| Timestamp header | podium-timestamp, Unix milliseconds |
| Scheme | HMAC-SHA256 |
| Signed content | <timestamp>.<raw body> |
| Secret | Optional, set when you create the webhook. No secret means no signature |
| Payload | { data, metadata: { event_ |
| Response timeout | 5 seconds |
| Queue | Events queue from creation, even while disabled, and expire after about 10 days |
| HTTP method | Not stated in Podium's docs |
| Retries | No schedule documented |
| Source IPs | Not published |
Common events
Event types take the form API_OBJECT.ACTION. Podium's webhook object lists 26:
| Area | Event types |
|---|---|
| Messages | message.received, message.sent, message.failed |
| Contacts | contact.created, contact.updated, contact.deleted, contact.merged, contact.unchanged |
| Reviews | review.created, review.updated, review.response_created, review.response_updated, review.invite_, review.invite_ |
| Calls | call.received, call.completed, call.missed, call.voicemail_left |
| Invoices | invoice.created, invoice.disabled, invoice.marked_as_paid, invoice.payment_created, invoice.payment_, invoice.payment_failed, invoice.refund_created, invoice.refund_failed |
message.failed means Podium accepted a message request but couldn't send it to the customer. contact.unchanged fires when you try to create or update a contact but it, or the change, already exists. Podium's pages don't describe the call and invoice types beyond their names.
Podium doesn't publish a full example event. The only body in its docs is the illustration used for signing:
{"data":{"bar":"apples","foo":"1"},"metadata":{"event_type":"test.event","version":"2021.04.01"}}
It shows the envelope: the object in data, and the event type and a version in metadata. Route on metadata.event_type.
Setting up Podium webhooks
- Get an access token from a user of the location or organization, with the scopes for the event types you need.
- Create a webhook through the webhook API with your
url, theeventTypes, thelocationUidororganizationUid, and asecret. Choose a long random value for the secret and store it, for example asPODIUM_WEBHOOK_SECRET. - Start your receiver promptly. Podium begins queueing events as soon as the webhook exists, even if it's disabled.
With Hookdeck. Podium is a supported source type in Hookdeck. Set a secret when you create the Podium webhook: webhooks created without one are unsigned and fail verification. Paste the same secret into a Hookdeck source of the Podium type and use the Hookdeck source URL as the webhook's url. The source checks podium-signature against <podium-timestamp>.<raw body>. It pre-selects POST without managing the method. Podium's docs don't say which method deliveries use, so if the request log shows another method, allow it on the source as well, because the source only accepts the methods you allow. A request that fails the check still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.
Securing Podium webhooks
Join the podium-timestamp header, a literal ., and the raw body bytes. Compute an HMAC-SHA256 with the secret as a UTF-8 string and compare it with podium-signature in constant time.
const crypto = require('crypto');
const express = require('express');
function verifyPodiumSignature(rawBody, signature, timestamp, secret) {
if (!signature || !timestamp || !secret) return false; // unsigned webhooks fail
if (!/^[0-9a-f]{64}$/i.test(signature)) return false; // validate before decoding
const signed = Buffer.concat([Buffer.from(`${timestamp}.`, 'latin1'), rawBody]);
const expected = crypto.createHmac('sha256', secret).update(signed).digest();
const received = Buffer.from(signature, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
// Optional replay check, off by default (0). podium-timestamp is in MILLISECONDS.
const TOLERANCE_SECONDS = Number(process.env.PODIUM_WEBHOOK_TOLERANCE_SECONDS || 0);
const app = express();
app.all('/webhooks/podium', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.PODIUM_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
const timestamp = req.get('podium-timestamp');
if (!verifyPodiumSignature(req.body, req.get('podium-signature'), timestamp, secret)) {
return res.sendStatus(401);
}
if (TOLERANCE_SECONDS > 0 && Math.abs(Date.now() - Number(timestamp)) / 1000 > TOLERANCE_SECONDS) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Route on event.metadata.event_type, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
import re
HEX_64 = re.compile(r"[0-9a-fA-F]{64}")
def verify_podium_signature(raw_body: bytes, signature, timestamp, secret: str) -> bool:
if not signature or not timestamp or not secret:
return False
if not HEX_64.fullmatch(signature):
return False
signed = timestamp.encode("latin-1") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
return hmac.compare_digest(bytes.fromhex(signature), expected)
Podium describes the scheme in steps and screenshots rather than code. The screenshots show a 64-character lowercase hex podium-signature, which is what the samples above and Hookdeck's Podium source expect. Podium doesn't recommend a replay window. If you add one, remember the timestamp is in milliseconds, and that Hookdeck applies no timestamp tolerance and its retries and replays resend the original headers.
Make Podium webhooks production-ready. Hookdeck Event Gateway verifies the
podium-signatureheader, answers Podium well inside its 5-second timeout, and stores every message, review and invoice event until your service has processed it.
Podium webhook limitations and pain points
Unsigned unless you set a secret
The Problem: Deliveries arrive with no podium-signature, so you can't prove they came from Podium.
Why It Happens: The secret is optional when you create a webhook, and a webhook without one is never signed.
Workarounds:
- Always create webhooks with a secret, and reject requests that lack the signature.
- If an existing webhook has no secret, create a new one with a secret and delete the old one.
How Hookdeck Can Help: Hookdeck's Podium source requires the signature, so unsigned requests are recorded as failed verification and never reach your service.
A 5-second timeout and an undocumented retry policy
The Problem: Slow handlers cause failed deliveries, and it isn't clear when Podium tries again.
Why It Happens: Podium says responses time out after 5 seconds. It describes a queue that holds events for around 10 days but doesn't publish a retry schedule.
Workarounds:
- Return
2xxas soon as the signature checks out, then process asynchronously. - If your endpoint is down for a long time, plan to backfill from the API, because events older than about 10 days expire from the queue.
How Hookdeck Can Help: Hookdeck answers Podium immediately and stores each delivery, then delivers to your service with automatic retries on a schedule you control, so a slow handler or an outage doesn't run into Podium's limits.
Disabled webhooks still fill the queue
The Problem: Re-enabling a webhook releases a backlog of old events, or events are lost after a long pause.
Why It Happens: Podium adds events to a webhook's queue from the moment it's created, even while it's disabled, and keeps them for about 10 days.
Workarounds:
- Expect a burst after re-enabling, and make handlers idempotent.
- Don't leave a webhook disabled for more than 10 days if you need its events.
How Hookdeck Can Help: Hookdeck can rate limit delivery to your service, so a backlog arrives at a pace your service can handle.
Sparse payload documentation
The Problem: It's hard to write a handler for an event you haven't seen.
Why It Happens: Podium publishes no example event bodies, and the call and invoice event types have no description pages.
Workarounds:
- Subscribe a test location and capture real events before writing parsing code.
- Treat fields as optional and log unknown event types instead of failing.
How Hookdeck Can Help: Hookdeck stores every request with its full body and headers, so you can inspect real Podium events and replay them against your handler while you build it.
Best practices
Verify against the raw body
The signature covers the body exactly as sent. Use express.raw() in Express, await request.arrayBuffer() in a Next.js route handler, and await request.body() in FastAPI.
Route on metadata.event_type
One webhook can carry many event types. Switch on metadata.event_type, and answer 2xx to types you don't handle so they don't count as failures.
Mind the millisecond timestamp
podium-timestamp is in milliseconds, unlike most webhook timestamps. Divide by 1,000 before comparing it with a clock in seconds.
Deduplicate
Podium doesn't document an event ID. Build a key from metadata.event_type, the object's ID in data and podium-timestamp, and skip repeats. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Verify, store the event, return 200, then do the work. See why to process webhooks asynchronously.
Conclusion
Podium signs webhooks with podium-signature, an HMAC-SHA256 of <podium-timestamp>.<raw body>, but only when the webhook was created with a secret. Always set one, verify against the raw bytes, remember the timestamp is in milliseconds, route on metadata.event_type, and answer within 5 seconds.
Hookdeck Event Gateway sits in front of your receiver, checks every Podium signature, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Podium webhooks reliably in minutes.