Coastal Home Services · case study

A lead form that knows when each lead synced.

WPForms, a hand-written webhook and REST callback, and a Make.com scenario with error recovery and an audit log. Built end to end and checked with a browser submission.

Status
Verified end to end
Built
Sept 2026
Runtime
WordPress 7.1 · PHP 8.4 · Docker
Engagement
Self-initiated

Leads were copied into the CRM by hand.

A local home-services company (HVAC, plumbing, electrical) takes quote requests through its website. Each one gets retyped into a spreadsheet or CRM, and nothing records whether that follow-up happened. A lead can sit untouched for a day and nobody can tell.

What finished looks like, stated so it can be checked:

  • Every submission lands in WordPress as a structured record with phone, service type and status.
  • The record reaches the follow-up workflow with no copy and paste.
  • The workflow reports back, so the record itself shows it was synced and when.

Coastal Home Services is invented. I wrote this brief for myself and built the whole stack against it on my own time.

Every lead ends synced, or the audit log records that it did not.

Automation

A Make.com scenario that recovers from its own failures

Five modules in one scenario. A failed callback is caught by a Resume handler, every run is written to an audit log, and only runs whose callback succeeded are exported to the spreadsheet.

WebhookHTTPResumeData storeSheets

Data model

Lead post type and fields, in code

register_post_type() and acf_add_local_field_group() in a plugin, so the schema is versioned with everything else and survives a theme change.

Outbound

Form submit to webhook

A named function creates the lead and posts a JSON payload with wp_remote_post(). It runs from wp eval, so it can be tested without a browser.

Inbound

A signed REST callback

POST /wp-json/coastal/v1/lead-status checks a shared secret with hash_equals(). A wrong secret gets 403; the right one marks the lead synced.

Front end

Landing page built in Elementor

The quote form sits in an Elementor layout seeded from a script through Document::save(), so a fresh install rebuilds the same page.

  • WordPress7.1
  • PHP8.4
  • MariaDB11.8
  • Docker Compose
  • ACF6.8
  • WPForms Lite2.0
  • Elementor4.2
  • REST API
  • Make.com
  • Google Sheets API
  • Cloudflare Tunnel

What happens after someone presses submit.

Every name in the diagram and the steps is copied from the code or from the Make scenario.

  1. CaptureWPForms fires wpforms_process_complete. The plugin creates a lead post with status new and fills the ACF fields.
  2. Hand offwp_remote_post() sends the lead id, name, phone, service and a callback URL to the Make webhook. If that request fails it is logged, and the visitor's form still succeeds.
  3. Call backMake's HTTP module posts the lead id to the REST route with the shared secret. WordPress sets status synced and stamps synced_at.
  4. RecoverIf the callback fails, a Resume handler substitutes statusCode: 0, so the run completes and the next modules can tell a recovered failure from a response.
  5. RecordEvery run is written to a Data Store audit log. A filter lets a run through to Google Sheets only when statusCode is not 0.

wp-content/plugins/coastal-core/includes/rest.php: the inbound callback

// shared-secret auth: timing-safe, no cookie or nonce (the caller is not a browser)
'permission_callback' => function (WP_REST_Request $req) {
    return hash_equals(
        getenv('COASTAL_WEBHOOK_SECRET'),
        $req->get_header('x-webhook-secret') ?? ''
    );
},

wp-content/plugins/coastal-core/includes/lead-webhook.php: the outbound half

function coastal_send_lead_webhook(int $lead_id, string $name, ...): void {
    $response = wp_remote_post(getenv('COASTAL_WEBHOOK_URL'), [
        'headers' => ['Content-Type' => 'application/json'],
        'body'    => wp_json_encode($payload),
    ]);
    // a down automation tool must never break the visitor's form
    if (is_wp_error($response)) { error_log(...); }
}
The lead pipelineThe dashed path runs only when the callback fails. The teal path is the one that reaches the spreadsheet.

One submission, followed through four screens.

The recording is one run; the three stills are a second run, for Marcus Delgado (HVAC), captured in wp-admin, Make and the spreadsheet in that order.

Leads that went through the pipeline, in order, times as logged.
TimeLeadNoteStatus
02:14Maria GonzalezElectrical. Sent to a stand-in endpoint before the scenario existed.Synced
02:37Robert ChenHVAC. First run through Make, synced in 2 s.Synced
03:41Sarah WilsonHVAC. The run in the recording.Synced
07:17Marcus DelgadoHVAC. The run in the three stills: synced, logged, exported.Synced
  • Submission to synced status, through Make~2 s
  • Callback with a wrong secret403
  • Callback with the right secret and an unknown lead404
  • Make modules and operations for one lead5 / 4
  • PHP warnings from project code, WP_DEBUG confirmed on0

Want to watch a lead go through it?

I can walk through the Make scenario, the audit log and the plugin code on a call. The source is public.