API Reference

Outbound Webhooks

Receive PlaceOptimizer events in your own services — endpoint registration, event types, payload shape, HMAC signature verification, retry behavior, and the test endpoint.

PlaceOptimizer can POST events to your own HTTPS endpoints when things happen inside the console — an audit completes, a post is published. Every delivery is signed so you can verify it really came from us, retried once on failure, and never blocked: your receiver's latency cannot slow down the console.

Webhooks are managed in the operator console at Dashboard → Webhooks (/dashboard/webhooks).

Registering an endpoint

In Dashboard → Webhooks, click Add endpoint:

  1. Endpoint URL — the https:// URL PlaceOptimizer will POST to. Plain http:// is refused.
  2. Events — the event types this endpoint subscribes to (at least one).

On creation you receive a signing secret exactly once — copy it and store it with the receiver. It is masked everywhere afterwards. Every delivery to that endpoint is signed with this secret, and you can rotate it by deleting and recreating the endpoint.

An endpoint can be paused with the enabled toggle without deleting it, and its URL or subscribed events can be edited afterwards.

Event types

EventFires whenPayload highlights
audit.completedAn audit run finishesrunId, score, businessName, locationCount, duplicates, cached
location.updatedA managed location changesreserved — not yet emitted
post.createdA Google Business Profile local post is publishedlocationRef, post
alert.createdA monitoring rule firesemitted once the alerts engine lands
schedule.sentA scheduled report email is deliveredreserved — not yet emitted

The list is the closed set of subscribable events. Emitted events are audit.completed and post.created today; the remaining types are emitted as their features ship — subscribing to them now is safe and they will simply not fire until then.

Payload shape

Every delivery is a single JSON object:

{
  "event": "audit.completed",
  "data": {
    "runId": "audit:peps-mattress:2026-07-22",
    "cached": false,
    "businessName": "Peps Mattress",
    "date": "2026-07-22",
    "locationCount": 5,
    "score": 78,
    "duplicates": 1
  },
  "deliveryId": "01KZXRX9RKXPQ7YER2HR2WTAK5",
  "timestamp": "2026-08-13T14:37:37.939Z"
}
  • deliveryId is unique per delivery attempt — use it for idempotency; a single logical event is delivered at most twice (see Retry behavior).
  • timestamp is the ISO-8601 time the envelope was built.

Request headers

Each POST carries four headers:

HeaderValue
content-typeapplication/json
x-placeoptimizer-eventthe event type (audit.completed, …)
x-placeoptimizer-deliverythe delivery id (same as deliveryId in the body)
x-placeoptimizer-signatureHMAC-SHA256 hex of the raw request body using the endpoint secret

Verifying the signature

The signature is an HMAC-SHA256 of the exact raw body bytes you received — never a re-stringified object. Verify it before trusting the payload, and reject any request whose signature does not match.

const crypto = require('node:crypto');

// The secret shown once when you created the endpoint.
const SECRET = 'whr_…';

async function handler(req, res) {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks); // keep the raw bytes!
  const signature = req.headers['x-placeoptimizer-signature'];

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(rawBody)
    .digest('hex');

  if (signature !== expected) {
    res.statusCode = 401;
    res.end('invalid signature');
    return;
  }

  const event = JSON.parse(rawBody.toString('utf8'));

  // Respond 2xx fast — PlaceOptimizer treats any 2xx as delivered.
  res.statusCode = 200;
  res.end('ok');

  processEvent(event); // do the real work after acknowledging
}

Use a constant-time comparison (crypto.timingSafeEqual on the hex buffers) in production. If you have not received your secret (it is only shown at creation), delete and recreate the endpoint.

Retry behavior

  • Each endpoint is delivered with a 10 second timeout.
  • On any failure (non-2xx response, network error, timeout) PlaceOptimizer retries exactly once, immediately, then records the outcome.
  • The Last delivery column on the endpoint row shows the outcome (Delivered / Failed) and time of the most recent attempt. There is no open-ended retry queue — a persistently failing endpoint stays Failed so you notice and fix it.

Because of the single retry, always make your receiver idempotent by deliveryId: the same event may arrive twice.

Testing an endpoint

The Test button on each endpoint row sends a real ping event synchronously and reports the actual result — it never fakes success:

{
  "event": "ping",
  "data": {
    "endpointId": "01J…",
    "url": "https://…",
    "events": ["audit.completed"]
  },
  "deliveryId": "01K…",
  "timestamp": "2026-08-13T14:37:37.939Z"
}

A test delivery is signed exactly like a real one, so you can verify your signature-checking code end to end. Test traffic does not update the endpoint's delivery ledger.

Best practices

  • Verify the signature on every delivery before acting on the payload.
  • Return 2xx quickly — PlaceOptimizer only cares about the status code; long-running work belongs after the response.
  • Idempotent receivers only: dedupe on deliveryId; the immediate retry can deliver the same event twice.
  • Use the test button after registering, and after changing your receiver, to confirm the whole path (including signature verification) works.
  • Treat the secret as a credential — it is shown once, and anyone holding it can forge deliveries your receiver will accept.
Copyright © 2026