Share & Connect

Receiving Webhooks

Receive webhook deliveries from Logspot, verify they came from us, and handle retries correctly.

A webhook destination sends an HTTP request to your endpoint when an action fires. This page is for whoever builds the receiving end.

To create one, add a Send Notification action and choose a webhook destination. For the opposite direction, sending events into Logspot, see Inbound Webhooks.

What We Send

Every delivery is a POST with a JSON body and these headers:

HeaderMeaning
x-logspot-event-typeThe event that fired the webhook
x-logspot-delivery-idUnique id for this delivery, stable across retries
x-logspot-signatureHex HMAC-SHA256 proving the request came from Logspot
x-logspot-signature-versionThe signing scheme, v1 or legacy
x-logspot-timestampUnix seconds when we signed, sent with v1 only
Content-Typeapplication/json

The default body names the event and the project:

{ "event": "Signup Completed", "projectId": "proj_123" }

If you configure a custom payload, that object is sent verbatim instead.

Custom headers you configure are sent too. They are merged before Logspot's signing headers, so a custom header cannot override the security headers. It can override Content-Type or x-logspot-event-type.

Your Signing Secret

Each webhook has its own secret, shown in the Signing Secret field when you set the webhook up. Treat it like a password, because anyone holding it can forge deliveries.

Editing a webhook's URL, headers, or payload does not change its secret. To roll one, recreate the webhook destination so a new secret is minted, then update your receiver. Deliveries sign with the new secret immediately, so plan for a brief overlap.

Verifying the Signature

Compute the HMAC yourself and compare it against x-logspot-signature.

For v1, which every new webhook uses, sign the string "{timestamp}.{body}", where the timestamp is the x-logspot-timestamp header and the body is the raw request bytes. For legacy, used by webhooks carried over from the older trigger system, sign the raw body alone and expect no timestamp header.

The body must be the exact bytes you received. A re-serialized JSON object will not match.

Reject v1 requests whose timestamp is too old. Five minutes is a sensible tolerance, and it is what stops a captured request being replayed later. Always compare with a constant-time comparison.

import crypto from 'node:crypto';

// `rawBody` must be the exact bytes received, e.g. via express.raw()
// or bodyParser's verify hook. A re-stringified object will NOT match.
export function verifyLogspotWebhook(headers, rawBody, secret) {
  const signature = headers['x-logspot-signature'];
  if (!signature) return false;

  const version = headers['x-logspot-signature-version'];
  const signedPayload =
    version === 'v1' ? `${headers['x-logspot-timestamp']}.${rawBody}` : rawBody;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  const a = Buffer.from(signature, 'hex');
  const b = Buffer.from(expected, 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;

  if (version === 'v1') {
    const ageSeconds = Math.abs(
      Date.now() / 1000 - Number(headers['x-logspot-timestamp']),
    );
    if (ageSeconds > 300) return false; // replay window: 5 minutes
  }
  return true;
}

Delivery Behavior

Respond with a 2xx quickly. Anything else counts as a failure, and we time out after five seconds. Acknowledge first, then do slow work.

Failures are retried on a backoff of roughly one minute, five minutes, thirty minutes, two hours, and eight hours. That is five retries across about ten and a half hours, after which the delivery is marked failed in the Actions activity log. Configuration errors such as an unreachable or blocked URL are not retried and fail immediately.

Deliveries are at-least-once. A retry can race a slow success, so duplicates are possible. Dedupe on x-logspot-delivery-id, which stays the same across every attempt of the same delivery.

There is no ordering guarantee, so do not assume deliveries arrive in event order.

Targets must be publicly reachable. URLs resolving to private or internal networks are refused both when the webhook is saved and when it would be delivered.