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_URLwith a standardized JSON payload. These are non-fatal -- if GHL is unreachable, the pipeline continues. Every outbound event is sent viasend_ghl_event()(inapp/api/webhooks.py), usually wrapped bydispatch_event()(inapp/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 includescore_typein 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_PEMinapp/api/ghl_inbound.py). IfX-GHL-Signatureis absent, the system falls back to legacy HMAC comparison viaX-GHL-Secretheader against theGHL_WEBHOOK_SECRETenv 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). |