HMAC-SHA256 signed
Outbound webhook delivery. Verify the `Fotowall-Signature` header against your event's webhook secret before trusting the body.
Signed webhooks for every photo, event, milestone, and guest signal. Bearer-auth callables for tenant-admin actions and GDPR exports. Anonymous token-gated callables for guest self-service. All real, all verified against the source tree.
Every endpoint picks one auth path based on what calls it. Webhook deliveries are signed outbound. Browser callables present a Firebase ID token. Anonymous flows (unsubscribe, gallery password) carry a short-lived HMAC token. Guest self-service (photo deletion) ties integrity to the upload session.
Outbound webhook delivery. Verify the `Fotowall-Signature` header against your event's webhook secret before trusting the body.
No authentication. Integrity is gated on a domain-specific check (uploader sessionId, gallery password, etc.). Rate-limited per IP.
No Firebase Auth required. Gated on a short-lived HMAC-signed token that the platform embeds in the relevant link (unsubscribe email, etc.).
Requires a Firebase Auth ID token with role=admin (or superadmin) custom claim. Tenant-scoped unless invoked by a superadmin with an explicit tenantId.
Anywhere that accepts POST + JSON over HTTPS. Zapier Catch Hook, Make Custom Webhook, Slack Incoming Webhook, a Cloud Run service, a tiny Lambda — all work as-is. No SDK required.
In the admin dashboard: event settings → Integrations → Webhooks. Paste the URL, pick which events fire it (photo.uploaded, photo.approved, etc.), and copy the generated webhook secret somewhere safe — you'll need it to verify the signature.
Hit "Send test event" in the admin. A signed photo.uploaded payload lands at your URL within a couple of seconds. Verify the Fotowall-Signature header (HMAC-SHA256 over the raw body), return 200, and you're live.
15 surfaces total — 8 outbound webhook events and 7 callable functions. Each link below opens the per-endpoint page with request and response schemas, curl + fetch examples, error cases, and a link to the source file in functions/src/.
OUTBOUND POST https://{your-endpoint} Signed HTTPS callbacks for every photo, event, milestone, and guest state change.
WEBHOOK EVENT photo.uploaded Fires when a guest uploads a photo. Also fires while moderation is on and the photo is still pending.
WEBHOOK EVENT photo.approved A moderator approved a pending photo OR an auto-approve event accepted an upload.
WEBHOOK EVENT photo.rejected A moderator rejected a pending photo. The photo is hidden from the wall and the upload count rolls back.
WEBHOOK EVENT event.published Your event moved to status="active". Uploads are accepted and the wall is rendering.
WEBHOOK EVENT event.archived The event was archived. Uploads are closed and the gallery is read-only.
WEBHOOK EVENT milestone.reached The event hit a photo-count threshold (100, 500, 1000, 5000, 10000).
WEBHOOK EVENT guest.joined A new uploader posted for the first time. Useful for greeting flows.
POST /getEmailPreferences Read a guest's email-preference flags by HMAC token (unauthenticated, CAN-SPAM compliant).
POST /setEmailPreferences Update a guest's email preferences via the same HMAC token (CAN-SPAM / CASL / GDPR Art. 7(3) compliant).
POST /verifyGalleryPassword Server-side check for a private-gallery password. No Firebase Auth required.
We tune limits per endpoint based on what realistic usage looks like. Most fail-open — a Firestore blip on the rate-limit counter won't block a legitimate call. Exceptions are the recap and export endpoints, where the cooldown is enforced atomically against state writes.
| Endpoint | Limit | Behavior on breach |
|---|---|---|
webhooks (per event) | 20 URLs configured / event, 5s timeout / delivery, 4 attempts total | Excess URLs ignored. Delivery marked failed after 4 attempts. |
requestPhotoDeletion | 30 calls / IP / hour | HttpsError resource-exhausted. |
getEmailPreferences | 20 calls / IP / hour | HttpsError resource-exhausted. Fails open on counter error. |
setEmailPreferences | 20 calls / IP / hour AND 10 writes / token / 24h | HttpsError resource-exhausted. Fails open on counter error. |
verifyGalleryPassword | No hard cap (yet); App Check planned | — |
sendEventRecap | 1 send / event / 24h, up to 5000 recipients | HttpsError resource-exhausted. Cooldown enforced atomically. |
claimSubdomain | No hard cap (auth-gated) | — |
accountDataExport | 1 export / tenant / 6h, max 5000 signed photo URLs | HttpsError resource-exhausted. Cooldown enforced. |
Callable functions throw Firebase HttpsError values that
serialize over the wire as a structured JSON object. Webhook deliveries
have no error envelope — your endpoint's response code dictates retry behavior.
{
"error": {
"code": "permission-denied",
"message": "Admin role required to export tenant data.",
"status": "PERMISSION_DENIED"
}
} | Code | Meaning |
|---|---|
unauthenticated | Missing or invalid Firebase Auth ID token. |
permission-denied | Token is valid but the caller lacks the required role / tenant scope. |
invalid-argument | Request body failed validation (missing field, wrong type, regex mismatch). |
not-found | Referenced doc (event, tenant, photo) does not exist. |
aborted | Conflict on an atomic operation (e.g., recap-in-progress lock held). |
resource-exhausted | Rate limit or cooldown was hit. |
internal | Server-side error (Firestore, Storage, Resend). Sentry-reported. Usually safe to retry. |
Breaking changes, new endpoints, deprecations. See the platform changelog for everything — API-affecting entries are tagged.
Open changelogAPI questions, schema requests, partner integrations. Founder reads every ticket; usual reply window is one business day.
Open a ticketFound a vulnerability? Coordinated disclosure path lives on the trust page. Don't post it publicly.
Trust pageNot yet. The callable endpoints work with the Firebase Functions SDK (recommended — it handles ID tokens and payload framing). The webhook delivery format is a plain signed HTTPS POST, so any HTTPS-capable stack consumes it. A first-party JS SDK is on the roadmap; meanwhile, plain fetch is the path.
https://us-central1-freedomgrc-photowall.cloudfunctions.net/{functionName}. We host in us-central1 only — single region keeps latency predictable and rules simple. Multi-region failover is a Premier/Enterprise conversation.
Per-endpoint, per-IP, and per-tenant — see each endpoint page for the specific cap. Most fail-open (a Firestore blip won't block a legitimate request). sendEventRecap enforces a hard 24h cooldown per event. accountDataExport enforces 6h per tenant.
Open the admin dashboard, pick the event, and copy the URL slug after ?event=. Or look up events/{slug} in Firestore directly if you have console access.
Open a ticket at /contact with `topic=api` — the API surface is small and we prioritize integrator feedback. Security issues: see /trust for the disclosure path.
Or hand this page to your integrations engineer and let them ship.