Webhook delivery format
Signed HTTPS callbacks for every photo, event, milestone, and guest state change.
OUTBOUND POST https://{your-endpoint} 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
| Field | Type | Required | Description |
|---|---|---|---|
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
| Field | Type | Always present | Description |
|---|---|---|---|
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
| Code | When |
|---|---|
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.