GAP School Module 06 — Marketplace Syndication Lesson 6.3

Marketplace pushes can fail silently. An API can return a success response that contains an error body. A unit can be skipped because validation failed, with no one notified. A cron job that fires the push can be disabled by a caching plugin. Without logging every push attempt and checking the aggregate result on a schedule, failures accumulate invisibly — and listings on the marketplace get stale without anyone knowing.


The situation

After the feed architecture was working, the question was: how do you know if it’s still working next Tuesday? The push is async. There’s no user watching it succeed. An API credential that rotates, a marketplace that changes its endpoint, a quota limit hit during a bulk update — any of these produces failures with no visible signal unless there’s a log and something that reads the log.


What I did

The push log table

Every push attempt — success or failure — gets a row in a dedicated log table. The table is created on plugin activation:

SQL
CREATE TABLE IF NOT EXISTS [client]_feed_push_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, unit_id BIGINT UNSIGNED NOT NULL, marketplace VARCHAR(50) NOT NULL, status ENUM('success','failed','validation_error','skipped') NOT NULL, response_code SMALLINT NOT NULL DEFAULT 0, response_body TEXT, pushed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_unit_marketplace (unit_id, marketplace), KEY idx_pushed_at (pushed_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Logging every push attempt

PHP
function [client]_log_feed_push( int $unit_id, string $marketplace, string $status, int $response_code, string $response_body = '' ): void { global $wpdb; $wpdb->insert( '[client]_feed_push_log', [ 'unit_id' => $unit_id, 'marketplace' => $marketplace, 'status' => $status, 'response_code' => $response_code, 'response_body' => substr( $response_body, 0, 2000 ), // cap body size 'pushed_at' => current_time( 'mysql' ), ], [ '%d', '%s', '%s', '%d', '%s', '%s' ] ); } function [client]_push_to_marketplace( int $unit_id, string $marketplace_id, array $record ): void { $creds = [client]_get_marketplace_credentials( $marketplace_id ); $payload = [client]_map_feed_record( $marketplace_id, $record ); $response = [client]_call_marketplace_api( $marketplace_id, $payload, $creds ); $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); if ( is_wp_error( $response ) || $code < 200 || $code >= 300 ) { [client]_log_feed_push( $unit_id, $marketplace_id, 'failed', (int) $code, $body ); [client]_alert_on_push_failure( $unit_id, $marketplace_id, $code, $body ); return; } [client]_log_feed_push( $unit_id, $marketplace_id, 'success', (int) $code, '' ); }

Immediate alert on failure

When a push fails, the admin gets an email immediately with the unit, marketplace, HTTP response code, and the full response body. The response body is what tells you whether it’s a credential issue, a quota issue, or a malformed payload:

PHP
function [client]_alert_on_push_failure( int $unit_id, string $marketplace, int $code, string $body ): void { $unit_title = get_the_title( $unit_id ); $admin_url = admin_url( 'admin.php?page=[client]_feed_log&unit_id=' . $unit_id ); $subject = "[client] Feed Push Failed: {$unit_title} on {$marketplace}"; $message = "A marketplace push failed.\n\n" . "Unit: {$unit_title} (ID: {$unit_id})\n" . "Marketplace: {$marketplace}\n" . "HTTP Code: {$code}\n\n" . "Response body:\n{$body}\n\n" . "View log: {$admin_url}"; wp_mail( get_option( 'admin_email' ), $subject, $message ); }

Daily stale-listing audit

Immediate alerts catch individual failures. The daily audit catches the cases the immediate alert misses: a cron that was disabled for 48 hours, a unit that was never pushed because its initial push happened during an API outage with no retry, or a marketplace that stopped accepting updates without returning an error.

PHP
// Register the daily audit add_action( 'wp', function() { if ( ! wp_next_scheduled( '[client]_daily_feed_audit' ) ) { wp_schedule_event( strtotime( 'tomorrow 07:00' ), 'daily', '[client]_daily_feed_audit' ); } } ); add_action( '[client]_daily_feed_audit', '[client]_audit_stale_listings' ); function [client]_audit_stale_listings(): void { global $wpdb; // Find active units with no successful push in the last 25 hours $stale = $wpdb->get_results( "SELECT DISTINCT p.ID, p.post_title, l.marketplace FROM {$wpdb->posts} p INNER JOIN [client]_feed_push_log l ON l.unit_id = p.ID WHERE p.post_type = '[client]_unit' AND p.post_status = 'publish' AND l.status != 'success' AND NOT EXISTS ( SELECT 1 FROM [client]_feed_push_log s WHERE s.unit_id = p.ID AND s.marketplace = l.marketplace AND s.status = 'success' AND s.pushed_at >= DATE_SUB( NOW(), INTERVAL 25 HOUR ) ) ORDER BY p.post_title" ); if ( empty( $stale ) ) { return; } $lines = array_map( fn( $r ) => "- {$r->post_title} (ID: {$r->ID}) on {$r->marketplace}", $stale ); $message = "The following units have not pushed successfully in the last 25 hours:\n\n" . implode( "\n", $lines ) . "\n\nReview the feed log: " . admin_url( 'admin.php?page=[client]_feed_log' ); wp_mail( get_option( 'admin_email' ), '[client] Stale Listings Audit', $message ); }

Why it matters

The immediate alert and the daily audit serve different failure modes. The immediate alert fires on errors that return something — a bad response code, a WP_Error, a non-2xx response. The daily audit fires on absence — a unit that should have pushed but didn’t, because a cron was disabled, because validation silently rejected it, because a push was queued and never executed.

The 25-hour window in the audit accounts for small timing drifts in daily processes while still catching units that were missed for a full day.


The Anchor build

The push log answered three real incidents: a credential rotation where the old API key was still in the database for 48 hours after the new one was set, a bulk price update that hit a rate limit on the third marketplace and failed silently on units 51–130, and a caching-plugin update that disabled WP-Cron for 18 hours. In all three cases, the log provided the exact timeline and scope of the failure. Without it, the incidents would have required checking four marketplace dashboards manually to determine what was out of date.


Do this, not that

  • Log success AND failure. A log that only captures failures doesn’t answer “when did this unit last push successfully?” Log every attempt. The success rows are as important as the failure rows.
  • Include the full response body in failure logs. HTTP 422 or HTTP 400 from a marketplace API always includes a JSON body that explains the specific error. A log that only stores the status code is half as useful.
  • Run a daily audit in addition to immediate alerts. Immediate alerts catch errors. The daily audit catches absence — the silent non-push that immediate alerting can’t see.
  • Set the stale window slightly above 24 hours. A 24-hour window will fire false positives on cron timing drift. 25 hours gives buffer while still catching genuine day-long failures.
  • Include a direct admin link in all alert emails. An alert that requires the recipient to navigate to find the relevant log loses half its value. The email should link directly to the failing unit’s log entries.
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 →