GAP School Module 06 — Marketplace Syndication Lesson 6.4

The outbound feed is only half the loop. When a buyer submits an inquiry on Boat Trader, that lead arrives in the dealership’s email inbox with no CRM connection, no source tracking, and no automatic response. The reverse channel closes the loop: a webhook endpoint that receives the inquiry, normalizes it to the platform’s format, deduplicates against existing leads, creates a CRM entry with source attribution, and enrolls the lead in a response sequence — all within seconds of the buyer clicking Submit.


The situation

Before the reverse channel, marketplace inquiries arrived in a generic email inbox. The team would manually copy the lead info into the CRM, manually note which marketplace it came from, and manually send a response. Average response time: 11 hours. Leads from multiple marketplaces for the same buyer were duplicated in the CRM. Source attribution for reporting was essentially nonexistent.


What I did

Unified REST endpoint with marketplace routing

A single REST endpoint handles all marketplaces. The marketplace identifier is in the URL, which lets each marketplace send to its own webhook URL while the handler code stays in one place:

PHP
add_action( 'rest_api_init', function() { register_rest_route( '[client]/v1', '/inquiry/(?P<marketplace>[a-z_]+)', [ 'methods' => 'POST', 'callback' => '[client]_handle_marketplace_inquiry', 'permission_callback' => '__return_true', // Auth handled inside handler ] ); } );

Webhook signature verification

Before doing anything with the payload, the handler verifies the request is genuinely from the marketplace. Each marketplace uses a different verification scheme:

PHP
function [client]_verify_marketplace_webhook( string $marketplace, WP_REST_Request $request ): bool { $creds = [client]_get_marketplace_credentials( $marketplace ); switch ( $marketplace ) { case 'boat_trader': // HMAC-SHA256 signature in header $sig = $request->get_header( 'X-BT-Signature' ); $secret = $creds['api_secret'] ?? ''; $expected = 'sha256=' . hash_hmac( 'sha256', $request->get_body(), $secret ); return hash_equals( $expected, $sig ?? '' ); case 'yachtworld': // Shared secret token in header $token = $request->get_header( 'X-YW-Token' ); $expected = $creds['webhook_token'] ?? ''; return hash_equals( $expected, $token ?? '' ); default: return false; } }

Per-marketplace normalizer

Each marketplace sends inquiries in a different format. The normalizer translates each format to a common shape that the rest of the handler consumes:

PHP
function [client]_normalize_marketplace_inquiry( string $marketplace, array $payload ): array { switch ( $marketplace ) { case 'boat_trader': return [ 'first_name' => sanitize_text_field( $payload['LeadFirstName'] ?? '' ), 'last_name' => sanitize_text_field( $payload['LeadLastName'] ?? '' ), 'email' => sanitize_email( $payload['LeadEmail'] ?? '' ), 'phone' => sanitize_text_field( $payload['LeadPhone'] ?? '' ), 'message' => sanitize_textarea_field( $payload['LeadMessage'] ?? '' ), 'listing_id' => sanitize_text_field( $payload['ListingID'] ?? '' ), 'source' => 'boat_trader', ]; case 'yachtworld': $name_parts = explode( ' ', $payload['contact_name'] ?? '', 2 ); return [ 'first_name' => sanitize_text_field( $name_parts[0] ?? '' ), 'last_name' => sanitize_text_field( $name_parts[1] ?? '' ), 'email' => sanitize_email( $payload['contact_email'] ?? '' ), 'phone' => sanitize_text_field( $payload['contact_phone'] ?? '' ), 'message' => sanitize_textarea_field( $payload['inquiry_text'] ?? '' ), 'listing_id' => sanitize_text_field( $payload['listing_ref'] ?? '' ), 'source' => 'yachtworld', ]; default: return []; } }

Deduplication, lead creation, and sequence enrollment

PHP
function [client]_handle_marketplace_inquiry( WP_REST_Request $request ): WP_REST_Response { $marketplace = $request->get_param( 'marketplace' ); // Verify signature before processing if ( ! [client]_verify_marketplace_webhook( $marketplace, $request ) ) { // Return 200 even on auth failure — don't reveal that verification failed return new WP_REST_Response( [ 'ok' => false ], 200 ); } $payload = $request->get_json_params() ?? []; $inquiry = [client]_normalize_marketplace_inquiry( $marketplace, $payload ); if ( empty( $inquiry['email'] ) ) { return new WP_REST_Response( [ 'ok' => false, 'error' => 'no_email' ], 200 ); } // Deduplicate: check for existing lead with this email $existing = [client]_find_lead_by_email( $inquiry['email'] ); if ( $existing ) { // Add a note to the existing lead rather than creating a duplicate [client]_add_lead_note( $existing, "Repeat inquiry via {$marketplace}: {$inquiry['message']}" ); return new WP_REST_Response( [ 'ok' => true, 'action' => 'noted_existing' ], 200 ); } // Create new lead with source attribution $lead_id = [client]_create_lead( [ 'first_name' => $inquiry['first_name'], 'last_name' => $inquiry['last_name'], 'email' => $inquiry['email'], 'phone' => $inquiry['phone'], 'source' => $inquiry['source'], 'message' => $inquiry['message'], 'listing_sku' => $inquiry['listing_id'], 'status' => 'new', ] ); // Enroll in response sequence [client]_enroll_in_sequence( $lead_id, 'marketplace_inquiry' ); // Always return 200 — marketplace webhooks that receive non-200 retry aggressively return new WP_REST_Response( [ 'ok' => true, 'action' => 'created' ], 200 ); }

Why it matters

The 11-hour response time was a function of manual process: the lead arrived in email, someone had to read it, copy it to the CRM, and send a response. The reverse channel makes the response time a function of automation speed. The sequence starts within seconds of the inquiry arriving. By the time a staff member is aware the lead exists, the first contact has already been sent.

The always-return-200 rule matters for operational stability. Marketplace webhook systems retry aggressively on non-200 responses. A 403 on signature failure triggers dozens of retries within minutes, generating log noise and potential rate limits. Return 200 for all responses; let the payload indicate what happened.


The Anchor build

Four marketplace inquiry endpoints configured. Marketplace lead response time: 11 hours → under 6 minutes (the sequence sends the first message; staff gets notified simultaneously). Duplicate leads reduced by 61% — many buyers inquire on multiple platforms about the same boat. The “existing lead + note” path handles this cleanly. Source attribution made it possible to measure, for the first time, that YachtWorld generated 3× the inquiry volume of BoatUS at 60% of the cost.


Do this, not that

  • Always return HTTP 200 from webhook endpoints. Non-200 responses trigger retry loops from marketplace systems. Return 200 and communicate success/failure in the JSON body instead.
  • Verify signatures before processing. An unverified webhook endpoint is an open injection point. Anyone who discovers the URL can submit fake leads. Signature verification ties the request to the marketplace’s credentials.
  • Normalize to a canonical shape before any business logic. The normalizer is the only place that knows about Boat Trader’s LeadFirstName vs YachtWorld’s contact_name. Everything downstream works with the normalized shape.
  • Deduplicate by email before creating a new lead. A buyer who inquires on three platforms for the same boat should be one lead record, not three. Check email first; add a note to the existing record if the lead already exists.
  • Store source attribution on every lead. Without it, you can’t measure which marketplaces produce leads that convert. Source data is only available at creation time — it can’t be reconstructed later.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.

None of this gates a single lesson. The curriculum was free before you got here and it stays that way.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.

See GAP Concierge →