The unified endpoint that routes marketplace inquiries into the CRM. Normalizers, signature verification, deduplication, and source attribution.
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.
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.
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:
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
] );
} );
Before doing anything with the payload, the handler verifies the request is genuinely from the marketplace. Each marketplace uses a different verification scheme:
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;
}
}
Each marketplace sends inquiries in a different format. The normalizer translates each format to a common shape that the rest of the handler consumes:
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 [];
}
}
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 );
}
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.
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.
LeadFirstName vs YachtWorld’s contact_name. Everything downstream works with the normalized shape.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.
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 →