Skip to content

Webhook Event Reference

Technical reference for every webhook event AEO Bunny sends to and receives from GoHighLevel (GHL).


How Webhooks Work

AEO Bunny communicates with GHL in two directions:

  • Outbound (AEO Bunny --> GHL): At key pipeline milestones, the system sends an HTTP POST to the configured GHL_WEBHOOK_URL with a standardized JSON payload. These are non-fatal -- if GHL is unreachable, the pipeline continues. Every outbound event is sent via send_ghl_event() (in app/api/webhooks.py), usually wrapped by dispatch_event() (in app/notifications.py) which also creates an in-app notification.
  • Inbound (GHL --> AEO Bunny): A single purchase webhook endpoint receives POST requests from GHL when a customer completes a purchase. Authenticated via Ed25519 signature verification (X-GHL-Signature), with legacy HMAC (X-GHL-Secret) fallback.
sequenceDiagram
    participant GHL as GoHighLevel
    participant API as AEO Bunny API
    participant Pipeline as Pipeline Runner
    participant BG as Background Tasks

    Note over GHL,API: INBOUND (1 event)
    GHL->>API: POST /api/v1/webhooks/ghl/purchase
    API-->>GHL: 200 OK (PendingLead)

    Note over API,Pipeline: OUTBOUND (pipeline lifecycle)
    API->>GHL: project_started (onboarding)
    API->>GHL: photos_upload_needed (onboarding, alongside project_started)
    Pipeline->>GHL: research_in_progress (BI step begins)
    Pipeline->>GHL: bi_review_needed (gate pause)
    Pipeline->>GHL: strategy_complete (strategy done)
    Pipeline->>GHL: matrix_review_needed (gate pause)

    Note over Pipeline,BG: OUTBOUND (per-batch, repeats 5x)
    Pipeline->>GHL: content_writing_started (batch N begins)
    Pipeline->>GHL: batch_article_review_needed (gate pause)
    Note over Pipeline,API: OUTBOUND (batch 1 only — photo gate)
    Pipeline->>GHL: photos_gate_waiting (customer notification)
    API->>GHL: photos_complete (100th photo uploaded, gate auto-resumes)
    Pipeline->>GHL: assembly_in_progress (HTML build)
    Pipeline->>GHL: batch_html_review_needed (gate pause)
    Pipeline->>GHL: batch_ready_to_ship (deploy gate)

    Note over API,BG: OUTBOUND (customer actions)
    API->>GHL: content_approved (customer approves batch)
    API->>GHL: batch_deployed (deployment confirmed)
    BG->>GHL: visibility_measured / batch_visibility_measured
    API->>GHL: project_complete (all 5 batches shipped)

    Note over API,BG: OUTBOUND (revision flow)
    API->>GHL: revision_approval_needed / revision_auto_approved
    API->>GHL: revision_started (approved, executing)
    BG->>GHL: revision_complete / revision_failed
    API->>GHL: revision_rejected (operator rejects)
    API->>GHL: revision_proposal (legacy Review Agent plan)

    Note over API,BG: OUTBOUND (system events)
    Pipeline->>GHL: pipeline_failed
    API->>GHL: password_reset_requested
    BG->>GHL: readiness_intake_complete / readiness_critical
    BG->>GHL: readiness_post_deploy_complete
    BG->>GHL: visibility_score_drop (alert)
    API->>GHL: consultation_requested

Base Payload Schema

Every outbound event shares the same JSON structure, produced by send_ghl_event() in app/api/webhooks.py.

{
  "event":           "string  -- event name (e.g. 'project_started')",
  "audience":        "string  -- 'customer' | 'operator' | 'both'",
  "project_id":      "string  -- location UUID (the project identifier)",
  "business_name":   "string  -- display name of the business",
  "customer_email":  "string | null  -- customer's email (for CRM routing)",
  "operator_email":  "string | null  -- operator email (currently unused)",
  "portal_url":      "string | null  -- deep link to relevant portal page",
  "ghl_contact_id":  "string | null  -- GHL contact ID (for CRM record matching)",
  "message":         "string  -- human-readable notification text",
  "metadata":        "object  -- event-specific key/value data (default: {})"
}

Transport: HTTP POST to GHL_WEBHOOK_URL (env variable) with JSON content type. 10-second timeout. Failures are logged and swallowed.


Outbound Event Catalog

1. project_started

Field Value
When Immediately after customer onboarding completes
Source app/api/onboarding.py -- onboard_customer()
Audience customer
In-app notification No (excluded event)

Example payload:

{
  "event": "project_started",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": "ghl_abc123def456",
  "message": "We've started working on your project!",
  "metadata": {}
}

Suggested GHL automation: Send welcome email/SMS sequence with onboarding confirmation and next-steps timeline.


2. research_in_progress

Field Value
When Pipeline begins BI research step (Phase A, first step)
Source app/pipeline/runner.py -- _run_strategic_phase()
Audience customer
In-app notification No (excluded event)

Example payload:

{
  "event": "research_in_progress",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Our team is researching your market and competitors",
  "metadata": {}
}

Suggested GHL automation: Send progress update email ("We're researching your market...").


3. bi_review_needed

Field Value
When Pipeline pauses at the BI review quality gate (after BI agent finishes)
Source app/pipeline/runner.py -- _pause_at_gate() via _GATE_EVENT_MAP
Audience operator
In-app notification Yes -- "BI Research Ready" (action_required)

Example payload:

{
  "event": "bi_review_needed",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Project paused at gate_bi_review \u2014 review required",
  "metadata": {}
}

Suggested GHL automation: Slack/email notification to operator that a project needs BI review.


4. strategy_complete

Field Value
When Content strategy step finishes (50-page matrix created)
Source app/pipeline/runner.py -- _run_strategic_phase()
Audience customer
In-app notification No (excluded event)

Example payload:

{
  "event": "strategy_complete",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "We've mapped out your content strategy \u2014 50 pages planned",
  "metadata": {}
}

Suggested GHL automation: Send "strategy complete" progress email to customer.


5. matrix_review_needed

Field Value
When Pipeline pauses at the content matrix review gate
Source app/pipeline/runner.py -- _pause_at_gate() via _GATE_EVENT_MAP
Audience operator
In-app notification Yes -- "Content Matrix Ready" (action_required)

Example payload:

{
  "event": "matrix_review_needed",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Project paused at gate_matrix_review \u2014 review required",
  "metadata": {}
}

Suggested GHL automation: Slack/email notification to operator that content matrix needs review.


6. content_writing_started

Field Value
When A batch begins the article writing step (fires per batch, up to 5 times)
Source app/pipeline/runner.py -- _batch_write_articles()
Audience customer
In-app notification No (excluded event)

Example payload:

{
  "event": "content_writing_started",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Writing batch 2 of 5 \u2014 10 pages",
  "metadata": {}
}

Suggested GHL automation: Send batch-progress email ("We're writing your next 10 pages...").


7. batch_article_review_needed

Field Value
When Pipeline pauses at the article review gate after a batch's articles are written
Source app/pipeline/runner.py -- _pause_at_gate() via _GATE_EVENT_MAP
Audience operator (batch 1 before operator_reviewed) or customer (batches 2-5, or batch 1 after operator_reviewed)
In-app notification Yes -- "Batch {N} Content Ready" (operator) or "Content Ready for Review" (customer) (action_required)

Example payload:

{
  "event": "batch_article_review_needed",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Batch 2 articles ready for review",
  "metadata": {}
}

Suggested GHL automation: Email/SMS customer with link to review portal; or notify operator to review batch 1.


8. assembly_in_progress

Field Value
When A batch enters HTML build phase (steps 4-6: images, schema, assembly)
Source app/pipeline/runner.py -- _batch_build_html()
Audience customer
In-app notification No (excluded event)

Example payload:

{
  "event": "assembly_in_progress",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Styling batch 3 pages, processing images, adding schema markup",
  "metadata": {}
}

Suggested GHL automation: Send progress email ("We're building your final pages...").


9. batch_html_review_needed

Field Value
When Pipeline pauses at HTML review gate after a batch's pages are assembled
Source app/pipeline/runner.py -- _pause_at_gate() via _GATE_EVENT_MAP
Audience operator (batch 1 before operator_reviewed) or customer (batches 2-5, or batch 1 after operator_reviewed)
In-app notification Yes -- "Batch {N} Pages Built" (operator) or "Pages Ready for Review" (customer) (action_required)

Example payload:

{
  "event": "batch_html_review_needed",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Batch 1 HTML pages ready for review",
  "metadata": {}
}

Suggested GHL automation: Notify reviewer that styled pages are ready for final visual QA.


10. batch_ready_to_ship

Field Value
When Pipeline pauses at deploy confirmation gate after a batch passes HTML review
Source app/pipeline/runner.py -- _pause_at_gate() via _GATE_EVENT_MAP
Audience customer
In-app notification Yes -- "Ready to Deploy" (action_required)

Example payload:

{
  "event": "batch_ready_to_ship",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Batch 2 ready to deploy",
  "metadata": {}
}

Suggested GHL automation: Email customer with deployment instructions and link to download ZIP.


11. content_approved

Field Value
When Customer clicks "Approve All" on a batch (articles or HTML review gate)
Source app/api/customer.py -- approve_all_content() via dispatch_event()
Audience customer
In-app notification Yes -- "Content Approved" (success)

Example payload:

{
  "event": "content_approved",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Great! We're now building your final pages.",
  "metadata": {}
}

Suggested GHL automation: Send confirmation email ("Your content is approved, we're building pages!").


12. batch_deployed

Field Value
When Customer confirms deployment for a specific batch (per-batch confirmation endpoint)
Source app/api/batch.py -- confirm_batch_deployment() via dispatch_event()
Audience operator
In-app notification Yes -- "Batch {N} Deployed" (success)

Example payload:

{
  "event": "batch_deployed",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Batch 2 deployment confirmed",
  "metadata": {
    "batch_number": 2
  }
}

Suggested GHL automation: Log deployment milestone in CRM; trigger post-deployment follow-up sequence.


13. pages_deployed

Field Value
When Customer confirms deployment via the legacy (non-batch) deployment endpoint
Source app/api/deployment.py -- confirm_deployment() via dispatch_event()
Audience operator
In-app notification Yes -- "Pages Deployed" (success)

Example payload:

{
  "event": "pages_deployed",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Customer confirmed pages are live at https://apexroofing.com/services",
  "metadata": {
    "hub_page_url": "https://apexroofing.com/services"
  }
}

Suggested GHL automation: Trigger post-deployment review sequence and schedule first visibility scan follow-up.


14. visibility_measured

Field Value
When Post-deployment visibility scan completes (non-batch projects), or a scheduled scan completes
Source app/api/deployment.py -- _run_visibility_scan() via dispatch_event() · app/visibility/scan_scheduler.py -- scheduled scan runner via dispatch_event_standalone()
Audience customer
In-app notification Yes -- "Visibility Score Ready" (info)

Note: This event covers two measurement paths: post-deployment and scheduled. Only the scheduled scan includes metadata.score_type ("scheduled"). The post-deployment path does not include score_type in metadata.

Example payload:

{
  "event": "visibility_measured",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Post-deployment visibility score: 72/100",
  "metadata": {
    "composite_score": 72.3,
    "engine_scores": {
      "chatgpt": 68.5,
      "perplexity": 76.1
    }
  }
}

Suggested GHL automation: Email customer their visibility score with link to dashboard.


15. batch_visibility_measured

Field Value
When Post-deployment visibility scan completes for a batch-mode deployment
Source app/api/deployment.py -- _run_visibility_scan() via dispatch_event()
Audience customer
In-app notification Yes -- "Batch Visibility Scored" (info)

Example payload:

{
  "event": "batch_visibility_measured",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Post-deployment visibility score: 65/100",
  "metadata": {
    "composite_score": 65.0,
    "engine_scores": {
      "chatgpt": 60.2,
      "perplexity": 69.8
    },
    "batch_number": 3
  }
}

Suggested GHL automation: Email customer batch-specific visibility results.


16. project_complete

Field Value
When All 5 batches have been shipped (pipeline reaches Phase C completion)
Source app/pipeline/runner.py -- _run_batch_loop() via dispatch_event_standalone()
Audience both (sends to both customer and operator)
In-app notification Yes -- "Project Complete" (success) for both audiences

Example payload:

{
  "event": "project_complete",
  "audience": "both",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "All 5 batches have been shipped \u2014 your project is complete!",
  "metadata": {}
}

Suggested GHL automation: Send congratulations email, NPS survey, referral request, and upsell sequence trigger.


17. pipeline_failed

Field Value
When Pipeline encounters an unrecoverable error at any step
Source app/pipeline/runner.py -- run_pipeline() except handler via dispatch_event_standalone()
Audience operator
In-app notification Yes -- "Pipeline Failed" (warning)

Example payload:

{
  "event": "pipeline_failed",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Pipeline failed: Traceback (most recent call last):\n  File \"app/pipeline/...",
  "metadata": {}
}

Suggested GHL automation: Urgent Slack/PagerDuty alert to operator; open support ticket.


18. password_reset_requested

Field Value
When Customer requests a password reset via /api/v1/auth/request-reset
Source app/api/auth.py -- _process_reset_request()
Audience customer
In-app notification No (excluded event)

Example payload:

{
  "event": "password_reset_requested",
  "audience": "customer",
  "project_id": "",
  "business_name": "",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": "ghl_abc123def456",
  "message": "You requested a password reset. Click the link to set a new password.",
  "metadata": {
    "reset_link": "https://portal.scale50.com/reset-password?token=abc123..."
  }
}

Suggested GHL automation: Send password reset email with the link from metadata.reset_link. This is the ONLY way password reset links reach the customer (no direct email from AEO Bunny).


19. revision_proposal

Field Value
When Customer's Review Agent chat produces a revision plan (legacy conversational flow)
Source app/api/customer.py -- review_chat() via dispatch_event()
Audience operator
In-app notification Yes -- "Revision Requested" (action_required)

Example payload:

{
  "event": "revision_proposal",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Customer wants to adjust tone and add emergency service details",
  "metadata": {
    "plan": {
      "summary": "Customer wants to adjust tone and add emergency service details",
      "revisions": [
        {"article_index": 3, "instruction": "Make more urgent, add 24/7 mention"}
      ]
    },
    "validation_errors": []
  }
}

Suggested GHL automation: Notify operator to review revision plan in admin portal.


20. revision_approval_needed

Field Value
When Customer clicks "Finish Review" with edited articles and the revision cost exceeds auto-approve threshold
Source app/api/batch.py -- finish_batch_review() via dispatch_event()
Audience operator
In-app notification Yes -- "Revision Request" (action_required)

Example payload:

{
  "event": "revision_approval_needed",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Batch 2 revision needs approval ($4.50, 3 articles)",
  "metadata": {
    "batch_number": 2,
    "articles_edited": 3,
    "estimated_cost": 4.50,
    "dossier_updates": 2
  }
}

Suggested GHL automation: Notify operator via Slack/email with cost estimate; link to approvals page.


21. revision_auto_approved

Field Value
When Customer clicks "Finish Review" with edited articles and the revision cost is below auto-approve threshold
Source app/api/batch.py -- finish_batch_review() via dispatch_event()
Audience operator
In-app notification Yes -- "Revision Auto-Approved" (info). Template exists in NOTIFICATION_EVENT_MAP and dispatch_event() creates the in-app notification.

Example payload:

{
  "event": "revision_auto_approved",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Batch 1 revision auto-approved ($1.50, 1 articles)",
  "metadata": {
    "batch_number": 1,
    "articles_edited": 1,
    "estimated_cost": 1.50,
    "dossier_updates": 1
  }
}

Suggested GHL automation: Log auto-approval in CRM for cost tracking (informational only).


22. revision_started

Field Value
When Operator approves a batch revision (sent to both audiences separately)
Source app/api/revisions.py -- approve_revision() via dispatch_event() (two calls: one for operator, one for customer)
Audience operator and customer (dispatched separately, not "both")
In-app notification Yes -- "Revision Executing" (operator, info) and "Revisions In Progress" (customer, info)

Example payload (operator):

{
  "event": "revision_started",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Revision approved for Apex Roofing Solutions batch 2",
  "metadata": {}
}

Suggested GHL automation: Email customer that revisions are underway; internal tracking of revision execution start.


23. revision_complete

Field Value
When All articles in a batch revision have been successfully re-written
Source app/pipeline/revision_executor.py -- execute_batch_revision() via dispatch_event_standalone()
Audience customer
In-app notification Yes -- "Revisions Complete" (success)

Example payload:

{
  "event": "revision_complete",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Revisions complete for Apex Roofing Solutions",
  "metadata": {}
}

Suggested GHL automation: Email customer that revised content is ready for re-review.


24. revision_failed

Field Value
When One or more articles in a batch revision fail to re-write
Source app/pipeline/revision_executor.py -- execute_batch_revision() via dispatch_event_standalone()
Audience customer
In-app notification Yes -- "Revision Issue" (info)

Example payload:

{
  "event": "revision_failed",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Revisions failed for Apex Roofing Solutions",
  "metadata": {}
}

Suggested GHL automation: Notify customer that our team is investigating; alert operator to intervene.


25. revision_rejected

Field Value
When Operator rejects a customer's batch revision request
Source app/api/revisions.py -- reject_revision() via dispatch_event()
Audience customer
In-app notification Yes -- "Revision Update" (info)

Example payload:

{
  "event": "revision_rejected",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Revision request for Apex Roofing Solutions was reviewed",
  "metadata": {
    "rejection_reason": "Requested changes are out of scope for the current package"
  }
}

Suggested GHL automation: Email customer with rejection context and next steps; offer upsell if scope-related.


26. readiness_intake_complete

Field Value
When Website readiness check finishes after customer onboarding
Source app/readiness/engine.py -- ReadinessEngine.run() via dispatch_event_standalone()
Audience operator
In-app notification Yes -- "Readiness Check Done" (info)

Example payload:

{
  "event": "readiness_intake_complete",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Readiness check complete: 78/100",
  "metadata": {
    "composite_score": 78,
    "crawlability_grade": "pass",
    "schema_grade": "warn",
    "speed_grade": "pass",
    "structured_data_grade": "warn",
    "issues_found": 3
  }
}

Suggested GHL automation: Log readiness baseline in CRM; if low score, flag for manual website review.


27. readiness_critical

Field Value
When Intake readiness check scores below 40 or crawlability fails
Source app/readiness/engine.py -- ReadinessEngine.run() via dispatch_event_standalone()
Audience operator
In-app notification Yes -- "Readiness Critical" (warning)

Example payload:

{
  "event": "readiness_critical",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Critical website readiness issues detected",
  "metadata": {
    "composite_score": 28,
    "crawlability_grade": "fail",
    "issues_found": 7
  }
}

Suggested GHL automation: Urgent Slack alert to operator; pause project and contact customer about website issues.


28. readiness_post_deploy_complete

Field Value
When Website readiness check finishes after customer confirms deployment
Source app/readiness/engine.py -- ReadinessEngine.run() via dispatch_event_standalone()
Audience operator
In-app notification No (excluded event)

Example payload:

{
  "event": "readiness_post_deploy_complete",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Readiness check complete: 85/100",
  "metadata": {
    "composite_score": 85,
    "crawlability_grade": "pass",
    "schema_grade": "pass",
    "speed_grade": "pass",
    "structured_data_grade": "warn",
    "issues_found": 1
  }
}

Suggested GHL automation: Log post-deployment readiness in CRM; compare against intake baseline.


29. visibility_score_drop

Field Value
When Visibility score drops significantly from peak (evaluated after every measurement)
Source app/visibility/alerts.py -- evaluate_alert() via dispatch_event()
Audience operator
In-app notification Yes -- "Visibility Dropped" (warning)
Cooldown One alert per location per alert_cooldown_hours (configurable, default 168h (7 days))

Example payload:

{
  "event": "visibility_score_drop",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Visibility score dropped to 45 (peak: 72)",
  "metadata": {
    "composite_score": 45.0,
    "peak_score": 72.3,
    "drop_amount": 27.3,
    "alert_type": "score_drop"
  }
}

Suggested GHL automation: Trigger upsell workflow; email customer about declining visibility with consultation offer.


30. consultation_requested

Field Value
When Customer requests a visibility consultation from the portal
Source app/api/consultation.py -- request_consultation() via dispatch_event()
Audience operator
In-app notification Yes -- "Consultation Requested" (info)
Cooldown One request per customer per 7 days (enforced in API)

Example payload:

{
  "event": "consultation_requested",
  "audience": "operator",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Customer requested consultation. Score: 45, Trend: declining",
  "metadata": {
    "composite_score": 45.0,
    "direction": "declining"
  }
}

Suggested GHL automation: Create task in CRM for operator to schedule consultation call; send confirmation email to customer.


31. photos_upload_needed

Field Value
When Immediately after customer onboarding completes (alongside project_started)
Source app/api/onboarding.py -- onboard_customer()
Audience customer
In-app notification Yes -- "Upload your business photos" (action_required). Note: dispatched via send_ghl_event() directly, so no in-app notification is actually created despite the template existing in NOTIFICATION_EVENT_MAP.

Example payload:

{
  "event": "photos_upload_needed",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": "ghl_abc123def456",
  "message": "Upload at least 100 photos of your business to build your pages.",
  "metadata": {
    "minimum_required": 100,
    "target": 150,
    "upload_url": "/portal/photos"
  }
}

Suggested GHL automation: Send email and SMS to customer with link to /portal/photos; enroll in reminder sequence until photos_complete fires.


32. photos_complete

Field Value
When Customer uploads their 100th photo, gate auto-resumes the pipeline
Source app/api/photos.py -- photo upload handler
Audience customer
In-app notification Yes -- "Photos received!" (success)

Example payload:

{
  "event": "photos_complete",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": "owner@apexroofing.com",
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Photos received! Building your pages now.",
  "metadata": {}
}

Suggested GHL automation: Stop photo reminder sequence; send "Photos received!" confirmation email to customer.


33. photos_gate_waiting

Field Value
When Pipeline has paused and is waiting for the customer to upload sufficient photos
Source app/pipeline/runner.py -- photo gate pause
Audience customer
In-app notification Yes -- "Photos needed to continue" (action_required)

Example payload:

{
  "event": "photos_gate_waiting",
  "audience": "customer",
  "project_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "business_name": "Apex Roofing Solutions",
  "customer_email": null,
  "operator_email": null,
  "portal_url": null,
  "ghl_contact_id": null,
  "message": "Waiting for photos — 42/100 uploaded",
  "metadata": {}
}

Suggested GHL automation: Send reminder email/SMS to customer with link to /portal/photos and current progress count.


Inbound Event Catalog

Purchase Webhook

GHL sends a POST request when a customer purchases an AEO Bunny package.

Authentication: Ed25519 signature verification is the primary auth method. GHL signs the request body with its private key and sends the signature in X-GHL-Signature. AEO Bunny verifies using GHL's hardcoded public key (_GHL_ED25519_PUBLIC_KEY_PEM in app/api/ghl_inbound.py). If X-GHL-Signature is absent, the system falls back to legacy HMAC comparison via X-GHL-Secret header against the GHL_WEBHOOK_SECRET env var. Migration from HMAC-only to Ed25519-primary is complete.

Field Value
Endpoint POST /api/v1/webhooks/ghl/purchase
Authentication Primary: X-GHL-Signature header (Ed25519 verification against hardcoded GHL public key). Fallback: X-GHL-Secret header (HMAC comparison against GHL_WEBHOOK_SECRET env var).
Rate Limiting None (GHL-initiated)
Idempotent Yes (re-posts update the existing PendingLead; duplicate email returns updated record)

Required headers:

X-GHL-Signature: <base64-encoded-ed25519-signature>   (preferred)
X-GHL-Secret: your-shared-webhook-secret               (legacy fallback)
Content-Type: application/json

Request body schema:

Field Type Required Description
email string (5-255 chars) Yes Customer email (auto-lowercased)
name string (1-255 chars) Yes Customer full name
phone string (0-50 chars) No Customer phone (defaults to "")
ghl_contact_id string (1-255 chars) Yes GHL contact record ID
product_tier string (1-50 chars) Yes Product tier purchased (e.g. "starter", "pro")

Example request:

{
  "email": "owner@apexroofing.com",
  "name": "James Mitchell",
  "phone": "+1-555-0123",
  "ghl_contact_id": "ghl_abc123def456",
  "product_tier": "pro"
}

Response codes:

Code Meaning
201 Created New PendingLead created
200 OK Existing PendingLead updated (re-post)
401 Unauthorized Invalid or missing signature — neither X-GHL-Signature (Ed25519) nor X-GHL-Secret (HMAC) passed verification
409 Conflict Customer already onboarded (PendingLead status is "converted")
422 Unprocessable Entity Request body validation failed

Example response (201):

{
  "status": "created",
  "lead_id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
}

What happens next: The customer receives an email (via GHL automation you configure) directing them to the onboarding form at /onboard. When they submit that form, the system verifies their PendingLead exists, creates their account, and starts the pipeline.


Field Glossary

Field Type Description
event string Machine-readable event name. Use this to route GHL automations.
audience string Who the event is intended for: "customer", "operator", or "both". Use this to decide which GHL workflow to trigger.
project_id string (UUID) The location UUID -- the primary identifier for a customer project. Maps to locations.id in the database. Empty string for non-project events (e.g. password_reset_requested).
business_name string Display name of the customer's business (e.g. "Apex Roofing Solutions"). Empty string for non-project events.
customer_email string or null The customer's email address. Present on most customer-facing events. Use for GHL contact lookup.
operator_email string or null Reserved for future use. Currently always null.
portal_url string or null Reserved for deep-linking. Currently always null at the GHL payload level (in-app notifications have their own action_url).
ghl_contact_id string or null The GHL contact record ID. Present when the customer was onboarded via the purchase webhook flow. Use for direct CRM record updates.
message string Human-readable notification text. Safe to display directly to the recipient or use as email body copy.
metadata object Event-specific structured data. Contents vary by event (see individual event docs above). Always an object, never null -- defaults to {}.
metadata.batch_number integer Batch number (1-5) for batch-scoped events.
metadata.composite_score float Visibility or readiness composite score (0-100).
metadata.engine_scores object Per-engine visibility scores (e.g. {"chatgpt": 68.5, "perplexity": 76.1}).
metadata.hub_page_url string URL the customer provided as their deployment hub page.
metadata.reset_link string One-time password reset URL (15-minute expiry).
metadata.estimated_cost float Estimated cost of a revision in dollars.
metadata.articles_edited integer Number of articles with customer feedback in a revision request.
metadata.rejection_reason string Operator's reason for rejecting a revision.
metadata.plan object Structured revision plan from the Review Agent (legacy flow).
metadata.validation_errors array Revision plan validation errors (empty array if valid).
metadata.minimum_required integer Minimum number of photos required to pass the photo gate (currently 100).
metadata.target integer Recommended photo count for best results (currently 150).
metadata.upload_url string Portal path where the customer can upload photos (e.g. /portal/photos).
metadata.total_photos integer Total photos uploaded by the customer at the time the event fired.
metadata.photo_count integer Current photo count at gate evaluation time (used in operator-facing events).