Gareth Wilson Gareth Wilson

Guide to Podium Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationThe webhook API resource only
ScopeExactly one location or organization per webhook
PermissionsA token from a user of that location or organization, with the scopes for the event types
Signature headerpodium-signature. 64 hex characters in Podium's screenshots; the encoding isn't stated in text
Timestamp headerpodium-timestamp, Unix milliseconds
SchemeHMAC-SHA256
Signed content<timestamp>.<raw body>
SecretOptional, set when you create the webhook. No secret means no signature
Payload{ data, metadata: { event_type, version } }
Response timeout5 seconds
QueueEvents queue from creation, even while disabled, and expire after about 10 days
HTTP methodNot stated in Podium's docs
RetriesNo schedule documented
Source IPsNot published

Common events

Event types take the form API_OBJECT.ACTION. Podium's webhook object lists 26:

AreaEvent types
Messagesmessage.received, message.sent, message.failed
Contactscontact.created, contact.updated, contact.deleted, contact.merged, contact.unchanged
Reviewsreview.created, review.updated, review.response_created, review.response_updated, review.invite_link_created, review.invite_link_updated
Callscall.received, call.completed, call.missed, call.voicemail_left
Invoicesinvoice.created, invoice.disabled, invoice.marked_as_paid, invoice.payment_created, invoice.payment_succeeded, 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

  1. Get an access token from a user of the location or organization, with the scopes for the event types you need.
  2. Create a webhook through the webhook API with your url, the eventTypes, the locationUid or organizationUid, and a secret. Choose a long random value for the secret and store it, for example as PODIUM_WEBHOOK_SECRET.
  3. 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-signature header, 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 2xx as 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.