Webhooks
Push, don't poll. When something happens in IntakeFlow we POST a signed JSON payload to every endpoint you've registered — perfect for Zapier, Make, n8n, your CRM, or your own backend.
Set it up
- Open Dashboard → user menu → Integrations & API → Outbound webhooks.
- Add your HTTPS URL (localhost is allowed for testing), optionally filtered to specific events.
- You'll see a
whsec_…signing secret once — store it. Send a test delivery to verify wiring before you rely on it.
Events
| portal.sent | A portal was sent to a client (dashboard, API, or MCP). |
| portal.submitted | The client submitted their portal for review. |
| portal.completed | Every item approved — the onboarding is done. |
| portal.reopened | Reserved for future use. |
| item.approved | One submitted item was approved. |
| item.rejected | One item was sent back with a note; the client is emailed. |
| client.created | A new client was added (dashboard, API, or MCP). |
Payload
Every delivery is a JSON object with the event name, your account id, a timestamp, and the data object:
POST https://your-app.com/hooks/intakeflow
{
"event": "portal.completed",
"account_id": "acc_…",
"created_at": "2026-09-30T14:22:09.481Z",
"data": {
"portal_id": "por_…",
"client": { "id": "b1e…", "name": "Acme Co", "email": "hello@acme.com" },
"completed_at": "2026-09-30T14:22:09.400Z",
"items": [ { "label": "Final logo files", "status": "approved" } ],
"source": "app"
}
}Verify the signature
Each delivery carries X-IntakeFlow-Signature: sha256=<hex> — an HMAC-SHA256 of the exact raw request body using your endpoint's signing secret. Always verify before trusting an event, and verify against the raw body before JSON parsing.
Node.js
import crypto from "crypto";
export async function POST(req: Request) {
const raw = await req.text();
const expected = "sha256=" + crypto.createHmac("sha256", process.env.INTAKEFLOW_WEBHOOK_SECRET).update(raw).digest("hex");
const got = req.headers.get("x-intakeflow-signature") ?? "";
const ok = expected.length === got.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
if (!ok) return new Response("bad signature", { status: 401 });
const event = JSON.parse(raw);
if (event.event === "portal.completed") {
// e.g. create the kickoff project in your PM tool
}
return Response.json({ ok: true });
}Retries & reliability
- Non-2xx responses (and network errors) retry up to 3 times with 1s/2s backoff.
- Every attempt is logged — see recent outcomes in the Integrations page, or via
GET /api/public/v1/webhooks. - Timeouts are 10 seconds per attempt; make your handler fast and enqueue work if needed.
Respond 2xx quickly, then process. A handler that does slow work before responding can look failed and trigger redundant retries — events may occasionally arrive more than once, so treat
data.portal_id + event as idempotency keys.Legacy completion URL
The platform-level PORTAL_COMPLETION_WEBHOOK_URL env fallback still fires for accounts without their own endpoints — but per-account endpoints are the way forward, and they cover far more events than completion alone.