Skip to main content

Acceptance Spec — Realtor Outbound Webhook Emitter (MVP)

  • Status: Proposed (MVP) — 2026-07-03
  • Product: WiseAI Realtor System → Integrations
  • Owner surface: /realtor/app/integrations (add/manage endpoints) + a server-side event emitter
  • Priority: NOW #3 (connective tissue — one build makes Zapier/Make/IXACT/Mailchimp/BoldTrail-push-in reachable without N connectors — AUDIT_realtor_integrations_strategy_2026-07-03.md §2 Automation, §5)
  • Related: ai-front-desk, realtor-calendar-booking-mvp, realtor-sms-messaging-mvp

1. Why (problem)

The founder's list wants IXACT (no real public API), BoldTrail (brokerage-gated API), Mailchimp (marketing), and Zapier/Make. Building a bespoke connector for each is wasteful. A single outbound webhook emitter — "on a domain event, POST a signed payload to a per-tenant URL" — lets an agent wire Aria into their own stack (Zapier/Make reach ~8,000 apps including IXACT and Mailchimp; BoldTrail accepts lead push-in). Highest leverage-per-build-hour on the roadmap. Today the Integrations hub honestly says "Outbound webhooks and a Zapier app aren't built yet."

2. Scope

In: per-tenant webhook endpoints (URL + secret + subscribed event types); a server-side emitter that fires on RE domain events; HMAC-signed JSON payloads; retry with backoff; a delivery log; a test-ping; and an SSRF-guarded URL validator. Manage from the Integrations screen.

Out (explicit non-goals): inbound/receiving webhooks; a full public REST API + API keys (LATER); a branded/published Zapier app (NEXT tier); customer-defined payload field mapping; delivering church/outreach events (RE spine only — the three-lead-worlds rule); emitting suppressed/withdrawn-consent contact PII.

3. Events (v1 catalog)

Minimal, high-value set (all RE-spine):

  • lead.created — a new re_contacts lead captured (voice/chat/QR/form).
  • lead.stage_changedre_contacts.pipeline_status transition.
  • showing.booked — a type='showing' task booked to a calendar (pairs with the calendar MVP).
  • review.requested — a Reviews Engine request created.

Payload (per event): { id, type, occurred_at, business_id, data: {...} } where data carries a minimal, non-sensitive projection (contact id, display name, source, language, pipeline stage, consent state) — never AI-summary intent notes, never financial/commission fields, never a withdrawn-consent contact's PII.

4. Data shape

re_webhook_endpoints
id uuid PK
business_id uuid NOT NULL FK
url text NOT NULL -- https only, SSRF-validated (§5)
secret_enc bytea NOT NULL -- per-endpoint signing secret, encrypted; shown to owner ONCE
events text[] NOT NULL -- subscribed event types
active boolean NOT NULL DEFAULT true
created_by text
created_at timestamptz NOT NULL DEFAULT now()
last_delivery_at timestamptz
UNIQUE (business_id, url)

re_webhook_deliveries
id uuid PK
endpoint_id uuid NOT NULL FK → re_webhook_endpoints(id) ON DELETE CASCADE
business_id uuid NOT NULL FK
event_type text NOT NULL
payload jsonb NOT NULL
attempts integer NOT NULL DEFAULT 0
status text NOT NULL DEFAULT 'pending' -- 'pending'|'delivered'|'failed'|'exhausted'
response_status integer
next_retry_at timestamptz
delivered_at timestamptz
created_at timestamptz NOT NULL DEFAULT now()

5. Security model (load-bearing — SSRF is the sharp edge)

  • SSRF guard on every endpoint URL (add AND each delivery): reject non-https; resolve the host and block private/loopback/link-local/metadata ranges (127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16 incl. the cloud metadata IP 169.254.169.254, ::1, ULA), block internal hostnames, and re-check on redirect (do not blindly follow redirects to internal hosts). This is non-negotiable — the platform already has an open http* SSRF finding flagged in the security backlog; a user-supplied webhook URL is a classic SSRF vector.
  • HMAC-SHA256 signature: every POST carries X-WiseAI-Signature: sha256=<hmac(secret, raw_body)> + X-WiseAI-Event + a timestamp; the receiver verifies with the per-endpoint secret. The secret is generated server-side, stored encrypted, and shown to the owner exactly once.
  • Delivery hardening: short timeout (e.g. 5s), capped retries with exponential backoff (e.g. 5 attempts → exhausted), no unbounded fan-out, per-tenant rate cap.
  • Consent/PII minimization: never emit a contact whose consent is withdrawn beyond the bare lead.stage_changed id; never emit AI intent notes or financial fields (§3).
  • Per memory: authed client calls carry ?account_id= (feedback_realtor_client_calls_need_account_id_query); endpoint config writes UPSERT (feedback_per_account_config_writes_must_upsert).

6. Flow (states)

  1. Add endpoint: owner pastes an https URL + picks event types → SSRF+https validation → generate secret (shown once) → a signed test-ping (type:'ping') must return 2xx before the endpoint is marked verified/active (honest-status-hub rule). A URL that fails SSRF/validation is rejected inline and never stored.
  2. Emit: on a domain event, enqueue a re_webhook_deliveries row per subscribed active endpoint; a worker POSTs the signed payload.
  3. Retry: non-2xx/timeout → increment attempts, set next_retry_at (backoff); after the cap → status='exhausted' and surface it in the delivery log.
  4. Manage: owner sees a delivery log (recent events, response codes, retries), can re-send, rotate the secret, disable, or delete an endpoint.

7. Honest status hub

The Integrations "Outbound webhooks / Zapier" card flips from "Not available yet" to active only when the tenant has ≥1 endpoint that passed the signed test-ping — never on a saved-but-unverified URL.

8. Acceptance criteria

  1. SSRF blocked: adding an endpoint pointing at http://, localhost, 127.0.0.1, 169.254.169.254, or a private-range host is rejected and never stored; a redirect to an internal host during delivery is not followed.
  2. Signed + verifiable: each POST carries X-WiseAI-Signature computed as HMAC-SHA256 of the raw body with the endpoint secret; a test receiver validates it.
  3. Test-ping gates activation: a new endpoint is only marked verified/active after a signed ping returns 2xx; a failing URL stays inactive.
  4. Emits on events: lead.created, lead.stage_changed, showing.booked, and review.requested each produce a delivery to every subscribed active endpoint, logged in re_webhook_deliveries.
  5. Retry + exhaust: a receiver returning 500 causes capped retries with backoff, ending in status='exhausted', visible in the log.
  6. PII minimization: payloads carry no AI intent notes, no financial/commission fields, and no withdrawn-consent contact PII (§3).
  7. Zapier reachability (proof of the whole point): a real Zapier "Catch Hook" (or Make) receives a lead.created event end-to-end and can route it onward (e.g. to a Google Sheet / IXACT), demonstrating the connector-free integration path.
  8. account_id + UPSERT: authed client calls carry ?account_id=; endpoint writes UPSERT.

9. Verification (deployed URL)

  • On the real host with the TeamMoelker showcase account: add an endpoint pointing at a Zapier Catch Hook (or a webhook.site receiver); confirm the signed test-ping activates it; trigger a lead.created (seed a lead) and assert the receiver got a correctly signed payload and a re_webhook_deliveries(delivered) row. Sample the delivery status at ≥2 timepoints (pending → delivered).
  • SSRF negatives: attempt to add http://169.254.169.254/… and http://localhost → assert both are rejected and unstored.
  • Retry: point an endpoint at a 500-returning receiver → assert backoff retries and a final exhausted state in the log.