Webhook delivery format

HMAC-SHA256 signed webhook event

Signed HTTPS callbacks for every photo, event, milestone, and guest state change.

METHOD OUTBOUND POST
EVENT TYPE https://{your-endpoint}
AUTH HMAC-SHA256 signed

Every webhook delivery is a POST to a URL you configure on the event. Each delivery is HMAC-SHA256 signed against the raw body using your event-scoped webhook secret. Retried 3 times (1s, 5s, 30s) on any non-2xx response before being marked failed. Up to 20 URLs per event. Because endpoints are configured per event, agencies route each activation's events to that client's systems — one client's lead capture to their CRM, another's to their DAM, fully isolated.

AUTH NOTE

Outbound webhook delivery. Verify the `Fotowall-Signature` header against your event's webhook secret before trusting the body.

Request

FieldTypeRequiredDescription
event string yes Event type (e.g. `photo.approved`). Mirrors the `Fotowall-Event-Type` header.
eventId string yes Your Fotowall event ID. Mirrors the `Fotowall-Event-Id` header.
deliveryId string yes Stable across retries — use for idempotency. Mirrors `Fotowall-Delivery-Id`.
timestamp string (ISO 8601 UTC) yes Server-side time the delivery doc was minted.
data object yes Event-specific payload. Shape varies by event type — see the seven event-type pages.

EXAMPLE BODY

{
  "event": "photo.approved",
  "eventId": "spring-gala-2026",
  "deliveryId": "del_01HV9X8T5R7K2M4Q6N8P0Z3W5J",
  "timestamp": "2026-05-17T03:14:42Z",
  "data": {
    "photo": {
      "id": "ph_8a92",
      "status": "approved",
      "url": "https://storage.googleapis.com/fotowall/.../ph_8a92.jpg",
      "uploaderName": "Maya",
      "approvedAt": "2026-05-17T03:14:42Z",
      "approvedBy": "[email protected]"
    }
  }
}

Response

FieldTypeAlways presentDescription
2xx any yes Any 2xx status acknowledges receipt and stops retries.
non-2xx / timeout any no Triggers the retry policy — 1s, 5s, 30s.

curl

# Verify a delivery on your side (Node example):
# Header looks like:  Fotowall-Signature: t=1715000000,v1=ab12cd34...

SECRET="whsec_your_event_secret"
SIGNED_PAYLOAD="${TIMESTAMP}.${RAW_BODY}"
EXPECTED=$(printf '%s' "$SIGNED_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

# Compare $EXPECTED to the v1= value in Fotowall-Signature in constant time.

JavaScript

We don't ship a first-party JS SDK yet (it's on the roadmap). For callable endpoints, the Firebase Functions SDK is the recommended path — it handles ID-token attachment and payload framing. Plain fetch works too.

// Send a TEST event from the admin "Send test event" button.
// (You normally don't construct webhooks yourself — the platform does.)
// This sample shows how to verify a delivery once it lands.

import crypto from 'node:crypto';

export function verifyFotowallSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const { t, v1 } = parts;
  if (!t || !v1) return false;
  // Reject anything older than 5 minutes (replay defense).
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const signed   = `${t}.${rawBody}`;
  const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(v1, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Error cases

CodeWhen
no retry 2xx response from your endpoint. Delivery marked success.
retry Non-2xx OR timeout (>5s). Delivery is retried per the policy.
failed All 4 attempts exhausted without 2xx. Delivery marked failed.
skipped Webhook URL is empty, non-HTTPS, or unparseable. Logged with a warning, no delivery attempted.

Need a different shape?

The API surface is small. Tell us what you need and we'll work backward from your integration.

Request an endpoint Back to API index