GAP School Module 06 — Marketplace Syndication Lesson 6.1

Pushing inventory to marketplaces sounds simple until you try to do it for more than one. Boat Trader expects one field name. YachtWorld expects a different one. BoatUS wants the price formatted differently. Without a canonical layer in the middle, each marketplace becomes its own implementation with its own error handling and its own drift path. With one, the marketplaces are interchangeable.


The situation

The Anchor site had inventory data in WordPress custom post meta. The marketplaces each had their own APIs with different field names, different authentication schemes, and different required vs. optional fields. A naive implementation would have had one push function per marketplace, each reading directly from post meta and mapping fields inline. Every bug fix, every field addition, would require touching all four functions. Adding a fifth marketplace would mean writing a fifth parallel implementation.

The feed architecture solves this with a canonical intermediate layer: a structured array that all four marketplace adapters read from, and a single function that builds it from post meta.


What I did

The canonical feed record

Every push starts by building a canonical record from the unit’s post meta. This is the single source of truth that all marketplace adapters consume:

PHP
function [client]_build_feed_record( int $unit_id ): array { return [ 'sku' => get_post_meta( $unit_id, '[client]_stock_number', true ), 'title' => get_the_title( $unit_id ), 'description' => get_post_meta( $unit_id, '[client]_ai_description', true ) ?: get_post_meta( $unit_id, '[client]_description', true ), 'price' => (int) get_post_meta( $unit_id, '[client]_price', true ), 'year' => (int) get_post_meta( $unit_id, '[client]_year', true ), 'make' => get_post_meta( $unit_id, '[client]_make', true ), 'model' => get_post_meta( $unit_id, '[client]_model', true ), 'length_ft' => (float) get_post_meta( $unit_id, '[client]_length', true ), 'hours' => (int) get_post_meta( $unit_id, '[client]_hours', true ), 'condition' => get_post_meta( $unit_id, '[client]_condition', true ), 'category' => get_post_meta( $unit_id, '[client]_category', true ), 'engine' => get_post_meta( $unit_id, '[client]_engine', true ), 'fuel_type' => get_post_meta( $unit_id, '[client]_fuel_type', true ), 'hull_type' => get_post_meta( $unit_id, '[client]_hull_type', true ), 'photos' => [client]_get_unit_photo_urls( $unit_id ), 'url' => get_permalink( $unit_id ), 'status' => get_post_meta( $unit_id, '[client]_status', true ), 'updated_at' => current_time( 'mysql' ), ]; }

Field validation before any API call

Before any marketplace adapter runs, the record is validated. A unit with a missing required field doesn’t trigger an API call — it gets flagged and skipped cleanly:

PHP
function [client]_validate_feed_record( array $record ): array { $required = [ 'sku', 'title', 'price', 'year', 'make', 'model', 'condition' ]; $issues = []; foreach ( $required as $field ) { if ( empty( $record[ $field ] ) ) { $issues[] = "Missing required field: {$field}"; } } if ( $record['price'] < 500 || $record['price'] > 5000000 ) { $issues[] = "Price out of range: {$record['price']}"; } if ( empty( $record['photos'] ) ) { $issues[] = 'No photos — most marketplaces require at least one'; } return $issues; }

Per-marketplace field mappers

Each marketplace gets its own mapper function. The mapper reads the canonical record and produces the payload shape that marketplace expects. Adding a new marketplace means adding one mapper, not touching anything else:

PHP
function [client]_map_to_boat_trader( array $record ): array { return [ 'ListingID' => $record['sku'], 'Title' => $record['title'], 'Description' => $record['description'], 'AskingPrice' => $record['price'], 'Year' => $record['year'], 'Make' => $record['make'], 'Model' => $record['model'], 'LengthFeet' => $record['length_ft'], 'HoursOnEngine' => $record['hours'], 'Condition' => ucfirst( $record['condition'] ), 'BoatType' => [client]_map_category_to_boat_trader( $record['category'] ), 'Images' => array_map( fn( $url ) => [ 'URL' => $url ], $record['photos'] ), ]; } function [client]_map_to_yachtworld( array $record ): array { return [ 'listing_reference' => $record['sku'], 'headline' => $record['title'], 'description_text' => $record['description'], 'asking_price_usd' => $record['price'], 'model_year' => $record['year'], 'builder' => $record['make'], 'model_name' => $record['model'], 'loa_feet' => $record['length_ft'], 'engine_hours' => $record['hours'], 'listing_condition' => $record['condition'], 'media_urls' => $record['photos'], ]; }

Async push orchestration

The push is triggered on save_post but runs asynchronously via wp_schedule_single_event. This keeps the save action fast (no API timeout on the admin user’s request) and lets WP-Cron handle the actual push in the background:

PHP
add_action( 'save_post_[client]_unit', '[client]_queue_marketplace_sync', 10, 2 ); function [client]_queue_marketplace_sync( int $post_id, WP_Post $post ): void { if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } // 5-second delay: lets the save complete before the cron fires wp_schedule_single_event( time() + 5, '[client]_push_to_all_marketplaces', [ $post_id ] ); } add_action( '[client]_push_to_all_marketplaces', '[client]_sync_to_marketplaces' ); function [client]_sync_to_marketplaces( int $unit_id ): void { $record = [client]_build_feed_record( $unit_id ); $issues = [client]_validate_feed_record( $record ); if ( ! empty( $issues ) ) { [client]_log_feed_push( $unit_id, 'all', 'validation_error', 0, implode( '; ', $issues ) ); return; } $marketplaces = [client]_get_active_marketplaces(); foreach ( $marketplaces as $marketplace_id ) { [client]_push_to_marketplace( $unit_id, $marketplace_id, $record ); } }

Why it matters

The canonical record layer is what makes the system maintainable. When Boat Trader changes a field name in their API, one mapper function changes. When you add a new field to inventory (e.g., hull_color), you add it to [client]_build_feed_record() once and it’s available to all four marketplace mappers. When you add a fifth marketplace, you write one mapper and one push function — the orchestration doesn’t change.

Validation before the API call matters for cost and reliability. An API call to a marketplace for a unit with no photos wastes the request, uses API quota, and may generate a confusing error response. Better to catch the missing photo before making the call and surface it as a clear validation error.


The Anchor build

4 marketplaces connected: Boat Trader, YachtWorld, BoatUS, and a regional aggregator. Average push latency from save_post to all four marketplaces updated: 12 seconds. Before this build, a unit marked sold on the website remained listed on the marketplaces for an average of 3.2 days. After: off-market across all platforms within 60 seconds of the status change.

Adding the fourth marketplace (the regional aggregator) required one mapper function, one push function, and adding a row to the active marketplaces config. Elapsed time: 28 minutes.


Do this, not that

  • Build a canonical feed record, not direct post-meta-to-API mapping. The intermediate layer is what makes the system maintainable. Direct mapping is 20% less code and 200% more fragile.
  • Validate before every API call. A unit with a missing required field should never trigger an API call. Catch it early, log it clearly, move on.
  • Push asynchronously via WP-Cron. Marketplace APIs are slow and sometimes down. A synchronous push on save_post means a staff member waiting 10 seconds for every save — and timing out when an API is unavailable.
  • One mapper per marketplace, nothing shared. Mappers look repetitive but must stay independent. If Boat Trader’s API changes, only the Boat Trader mapper changes. Shared helpers between mappers mean a fix in one adapter breaks another.
  • Log every push attempt, success and failure. The log is what answers “why isn’t this unit on YachtWorld?” when someone asks three weeks later. Without it, there’s no trail to follow.
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 →