Skip to content

WooCommerce Integration Guide

Welcome! This guide will help you integrate your WooCommerce store with the TBC CRM product catalog.


  1. API Credentials (provided by TBC team)

    • Client ID
    • Client Secret
    • Allowed scopes
  2. WordPress/WooCommerce Site

    • WordPress 5.0+
    • WooCommerce plugin installed
    • wpgetAPI plugin (recommended)
  3. Integration Time

    • Basic setup: 30 minutes
    • Full product sync: 1-2 hours

Ask your CRM administrator to create an M2M client for you (CRM web app → Settings → API Access). They will provide your Client ID, Client Secret, API base URL, and token endpoint.

You’ll receive:

Client ID: [26-character code]
Client Secret: [52-character secure code]
Scopes: crm-api/products.read

Section titled “Option A: Using wpgetAPI Plugin (Recommended)”

Best for: Non-technical users, quick setup

Pros:

  • Visual configuration (no coding)
  • Automatic OAuth 2.0 handling
  • Built-in token caching
  • Easy to maintain

Setup Time: 15-30 minutes

Jump to wpgetAPI Setup →


Best for: Developers, custom requirements

Pros:

  • Full control over sync logic
  • Customizable product mapping
  • Advanced error handling
  • Schedule flexibility

Setup Time: 1-2 hours

Jump to Custom PHP Setup →


  1. In WordPress admin, go to Plugins > Add New
  2. Search for “wpgetAPI”
  3. Click Install Now, then Activate
  1. Navigate to wpgetAPI > Add New
  2. Fill in the following:

Basic Settings:

API Name: TBC CRM API
Base URL: https://{api-base-url}/

Authentication:

Auth Type: OAuth 2.0
Grant Type: Client Credentials
Token URL: {token-endpoint}
Client ID: [Your Client ID]
Client Secret: [Your Client Secret]
Scope: crm-api/products.read

Advanced Settings:

Token Caching: Enabled (recommended)
Cache Duration: 3000 seconds (50 minutes; new clients get 1-hour tokens)
  1. Click Save
  1. In the same API settings, scroll to Endpoints
  2. Click Add Endpoint
  3. Configure:
Endpoint ID: get-products
Method: GET
Endpoint Path: products/search
Query String: size=50&updatedAfter=1970-01-01T00:00:00Z
  1. Click Save
  1. Click Test Endpoint next to get-products
  2. You should see a JSON response with product data
  3. If you see an error, verify your credentials and try again

Add this code to your theme’s functions.php or create a custom plugin:

<?php
function sync_tbc_products() {
$syncToken = null;
$ok = true; // false if the pass aborted before reaching the end
do {
$query_vars = array(
'size=50',
'updatedAfter=1970-01-01T00:00:00Z'
);
if ($syncToken) {
// CRITICAL: URL encode the token to prevent + becoming space
$query_vars[] = 'syncToken=' . urlencode($syncToken);
}
$args = array('query_variables' => implode('&', $query_vars));
$response = wpgetapi_endpoint('tbc-crm-api', 'get-products', $args);
if (is_wp_error($response)) {
error_log('TBC API Error: ' . $response->get_error_message());
$ok = false;
break;
}
// WPGetAPI returns EITHER a decoded array OR the raw JSON body depending
// on the endpoint's "Format results" setting. Normalise before inspecting.
if (is_string($response)) {
$response = json_decode($response, true);
}
// Distinguish FAILURE from END-OF-RESULTS. A successful response always
// carries a `data` array (an empty last page is `data: []`); no error
// response carries `data` at all. Treating an error as "no more products"
// is what makes a backfill stop silently partway — leaving some products
// without `_tbc_product_id` and the later incremental sync exposed to the
// duplicate problem this id is meant to prevent.
if (!is_array($response) || !isset($response['data']) || !is_array($response['data'])) {
$detail = is_array($response)
? ($response['error'] ?? $response['code'] ?? $response['message'] ?? 'unknown')
: 'malformed response';
error_log('TBC API Error: ' . $detail);
$ok = false;
break;
}
if (empty($response['data'])) {
break; // genuine end of the catalogue
}
foreach ($response['data'] as $product) {
// Match on the CRM's immutable `id` first. On the very first run
// nothing has it yet, so the SKU fallback does the matching and this
// pass BACKFILLS the id onto every product it touches — which is
// what makes the incremental sync below safe against SKU renames.
$existing = get_posts(array(
'post_type' => 'product',
'meta_key' => '_tbc_product_id',
'meta_value' => $product['id'],
'posts_per_page' => 1,
'post_status' => 'any', // get_posts() defaults to 'publish'
'fields' => 'ids',
));
$product_id = $existing ? $existing[0] : wc_get_product_id_by_sku($product['productCode']);
if ($product_id) {
$wc_product = wc_get_product($product_id);
} else {
$wc_product = new WC_Product_Simple();
}
// Update product
$wc_product->set_name($product['description']);
$wc_product->set_sku($product['productCode']);
// Use retailPrice (end-consumer RRP) for the WooCommerce storefront price.
// Fall back to price (trade price) if retailPrice is not set.
$wc_product->set_regular_price($product['retailPrice'] ?? $product['price'] ?? 0);
// notes is the public-facing short description (see the sync guide's field list).
$wc_product->set_short_description($product['notes'] ?? '');
$wc_product->save();
// Store the CRM id so later syncs match on identity, not on SKU.
update_post_meta($wc_product->get_id(), '_tbc_product_id', $product['id']);
}
// Get token for next page (check both locations)
$syncToken = $response['syncToken']
?? $response['pagination']['syncToken']
?? null;
} while ($syncToken);
// A backfill that aborted partway leaves products WITHOUT `_tbc_product_id`,
// which is exactly the state the incremental sync assumes is impossible.
// Say so loudly and re-run rather than proceeding as if it succeeded.
if (!$ok) {
error_log('TBC full sync did NOT complete — some products may lack _tbc_product_id. Re-run before relying on incremental sync.');
}
return $ok;
}
// Schedule daily sync at 2 AM
if (!wp_next_scheduled('tbc_daily_sync')) {
wp_schedule_event(strtotime('02:00:00'), 'daily', 'tbc_daily_sync');
}
add_action('tbc_daily_sync', 'sync_tbc_products');
?>

Done! Your products will now sync automatically every day at 2 AM.

[!NOTE] A “completed” pass is not a snapshot. syncToken paging reads a live index — a pass that ends cleanly can still have missed a product that was edited or indexed mid-pass, and can return the same product twice (harmless: the code above upserts by id). Never delete WooCommerce products because they were absent from one pass, and don’t expect archived CRM products to be withdrawn by the sync — they simply stop appearing. Full guarantees: What a completed full pass guarantees.


Option A (Alternative): Using “API to Posts” Extension

Section titled “Option A (Alternative): Using “API to Posts” Extension”

If you prefer a low-code solution, you can use the API to Posts extension (paid) for wpgetAPI.

  1. Install the API to Posts extension.
  2. Configure mapping (e.g., productCode -> _sku, price -> _regular_price).
  3. Important: Add this snippet to functions.php.

    [!WARNING] Check Your API ID: In the code below, replace 'tbc-crm-api' with the actual Unique ID of your API from the WPGetAPI settings page. If they don’t match, pagination will fail.

// Extract and URL-encode the syncToken from the response data
add_filter('wpgetapi_api_to_posts_pagination_next_value', 'tbc_pagination_key', 10, 4);
function tbc_pagination_key($next_value, $data, $api_id, $endpoint_id) {
if ($api_id === 'tbc-crm-api' && $endpoint_id === 'get-products') {
// $data is the API response body (2nd param per WPGetAPI docs)
if (is_string($data)) {
$data = json_decode($data, true);
}
// Check both root level and inside pagination object
$token = $data['syncToken']
?? $data['pagination']['syncToken']
?? null;
// No token = last page. Return null to stop pagination.
if (!$token) return null;
// CRITICAL: URL encode to prevent + becoming space
return urlencode($token);
}
return $next_value;
}

For developers who need full control, download our production-ready PHP client:

Download: TBC CRM API Client (PHP)

Features:

  • Automatic token caching (WordPress Transients)
  • Pagination handling (fetches all products)
  • Retry logic with exponential backoff
  • Detailed error messages

Basic Usage:

<?php
require_once('crm-api-client.php');
$client = new TBC_CRM_API_Client(
get_option('tbc_client_id'),
get_option('tbc_client_secret')
);
// Fetch all products (handles pagination automatically)
foreach ($client->get_all_products() as $product) {
// Your sync logic here
echo $product['name'] . "\n";
}
?>

Full Documentation: Advanced Integration Guide



For large catalogs (e.g., 200k+ products), a full daily sync is inefficient. The industry standard is Incremental Syncing, where you only fetch products that have changed since your last sync.

We support this via the /products/search endpoint using the updatedAfter parameter.

Add a new endpoint in wpgetAPI:

Endpoint ID: get-updates
Method: GET
Endpoint Path: products/search
Query String: size=50

Use this PHP snippet to fetch only changed products:

function sync_tbc_product_updates() {
// Get last sync time (default to long ago for first run)
$last_sync = get_option('tbc_last_sync_time', '2000-01-01T00:00:00Z');
// Capture the next checkpoint BEFORE fetching, already rewound by the
// overlap window. Deriving it from the data instead (e.g. the highest
// updatedAt seen) looks appealing but regresses: a pass that legitimately
// returns nothing has no highest value to use, so the checkpoint would
// walk backwards by the overlap on every quiet run and re-fetch an
// ever-growing window. A pre-fetch boundary only ever moves forward.
$overlap_seconds = 900; // 15 minutes — see "Why the overlap window matters"
$next_checkpoint = gmdate('Y-m-d\TH:i:s\Z', time() - $overlap_seconds);
$sync_token = null;
$updated_count = 0;
$ok = true; // Track sync success
do {
$query_vars = array(
'updatedAfter=' . urlencode($last_sync),
'size=50'
);
if ($sync_token) {
$query_vars[] = 'syncToken=' . urlencode($sync_token);
}
$args = array('query_variables' => implode('&', $query_vars));
$response = wpgetapi_endpoint('tbc-crm-api', 'get-updates', $args);
// Transport failure: WPGetAPI hands back a WP_Error object, which is NOT
// array-accessible. Handle it BEFORE any $response[...] read, or the error
// path itself fatals and the checkpoint guard below never runs.
if (is_wp_error($response)) {
error_log('TBC Incremental Sync Error: ' . $response->get_error_message());
$ok = false;
break; // abort WITHOUT committing the checkpoint
}
// WPGetAPI returns EITHER a decoded array OR the raw JSON body, depending
// on the endpoint's "Format results" setting. Normalise before inspecting,
// or a string-returning configuration makes every response look malformed
// and the sync aborts forever without ever committing a checkpoint.
if (is_string($response)) {
$response = json_decode($response, true);
}
// A successful response ALWAYS carries a `data` array; an empty final page
// is `data: []`. No error response carries `data` at all. Checking for the
// presence of success is robust to every error shape, including failures
// that carry no machine-readable code.
if (!is_array($response) || !isset($response['data']) || !is_array($response['data'])) {
$detail = is_array($response)
? ($response['error'] ?? $response['code'] ?? $response['message'] ?? 'unknown')
: 'malformed response';
error_log('TBC Incremental Sync Error: ' . $detail);
$ok = false;
break; // abort WITHOUT committing the checkpoint
}
if (empty($response['data'])) {
break;
}
foreach ($response['data'] as $product) {
// Match on the CRM's immutable `id`, NOT on the SKU. `productCode`
// is editable in the CRM, so a SKU-keyed lookup misses a renamed
// product and creates a duplicate that no later sync can reconcile.
$existing = get_posts(array(
'post_type' => 'product',
'meta_key' => '_tbc_product_id',
'meta_value' => $product['id'],
'posts_per_page' => 1,
'post_status' => 'any', // get_posts() defaults to 'publish'
'fields' => 'ids',
));
$product_id = $existing ? $existing[0] : 0;
// Fallback for products imported before _tbc_product_id was stored.
// Remove once your catalogue is fully backfilled.
if (!$product_id) {
$product_id = wc_get_product_id_by_sku($product['productCode']);
}
if ($product_id) {
$wc_product = wc_get_product($product_id);
} else {
$wc_product = new WC_Product_Simple();
}
$wc_product->set_name($product['description']);
$wc_product->set_sku($product['productCode']);
// Use retailPrice (end-consumer RRP) for the WooCommerce storefront price.
// Fall back to price (trade price) if retailPrice is not set.
$wc_product->set_regular_price($product['retailPrice'] ?? $product['price'] ?? 0);
// notes is the public-facing short description (see the sync guide's field list).
$wc_product->set_short_description($product['notes'] ?? '');
$wc_product->save();
// Record the CRM id so the next sync matches on identity, not SKU.
update_post_meta($wc_product->get_id(), '_tbc_product_id', $product['id']);
$updated_count++;
}
// Get token for next page of updates (check both locations)
$sync_token = $response['syncToken']
?? $response['pagination']['syncToken']
?? null;
} while ($sync_token);
// Commit the pre-fetch boundary ONLY on a clean pass. It already carries
// the overlap rewind, so a product saved just before this run but indexed
// just after it still falls inside the next run's window. See "Why the
// overlap window matters" in the M2M sync guide.
if ($ok) {
update_option('tbc_last_sync_time', $next_checkpoint);
error_log("TBC Incremental Sync: Updated {$updated_count} products. New checkpoint: {$next_checkpoint}");
}
}
// Schedule hourly incremental sync (more frequent than full sync)
if (!wp_next_scheduled('tbc_incremental_sync')) {
wp_schedule_event(time(), 'hourly', 'tbc_incremental_sync');
}
add_action('tbc_incremental_sync', 'sync_tbc_product_updates');

GET /products/search?size=50&brandName=GSI+Ceramica&syncToken=...
Authorization: Bearer {access_token}

Response:

{
"data": [
{
"id": "prod_abc123",
"productCode": "CH-BM-001",
"description": "Chrome Basin Mixer",
"brandName": "Grohe",
"category": "Mixers",
"price": 299.99,
"retailPrice": 399.99,
"notes": "Premium chrome basin mixer with ceramic disc cartridge",
"imageUrl": "https://...",
"createdAt": "2025-01-15T10:30:00Z"
}
],
"pagination": {
"total": 66,
"from": 0,
"size": 50,
"hasMore": true,
"syncToken": "WzE3NTYyOTA5NjAzMDks..."
},
"syncToken": "WzE3NTYyOTA5NjAzMDks..."
}
  • hasMore: true when more pages exist, false on the last page
  • syncToken: pass this as a query parameter to fetch the next page. null on the last page.

Price field semantics:

FieldDescriptionWooCommerce mapping
priceTrade/B2B selling price — what the CRM user charges their customers (e.g. contractors, resellers)Use as the baseline if your store sells at trade price
retailPriceRecommended retail price (RRP) — what end-consumers pay on the storefrontMap to regular_price for a consumer-facing WooCommerce store

supplierCost (the CRM user’s buy price) is returned — treat it as commercially sensitive and never render it on a storefront. Cost and margin fields other than supplierCost are never returned, and the caller cannot widen the field set.

GET /products/{id}
Authorization: Bearer {access_token}

Response:

{
"id": "prod_abc123",
"productCode": "CH-BM-001",
"name": "Chrome Basin Mixer",
"price": 299.99,
...
}

Two independent per-IP limits apply, and either returns 429 Too Many Requests:

  • API Gateway throttle: 50 requests per second sustained, 100 burst.
  • WAF rule: a single IP is blocked after ~1,000 requests in any rolling 5-minute window — and stays blocked until the window clears, so a short wait-and-retry won’t help. This is the limit that matters for a full catalog sync: page with size=100 and keep the sync under ~1,000 requests per 5 minutes (roughly 3/sec, sequential — don’t fan out parallel workers from one IP).

Best Practices:

  • Cache product data locally
  • Sync during off-peak hours (e.g., 2-4 AM)
  • Use the size parameter (max 100) for efficient pagination

On a 429, back off with increasing delays; if you tripped the WAF rule, expect the block to last for the remainder of the 5-minute window.


Problem: Invalid or expired access token

Solution:

  1. Verify Client ID and Client Secret are correct
  2. Check that credentials haven’t been revoked
  3. Ensure scope is crm-api/products.read
  4. Clear your token cache and try again

Problem: Insufficient permissions

Solution:

  1. Verify your scope includes crm-api/products.read
  2. Contact TBC support to confirm your access level

Problem: Empty product list

Solution:

  1. Check if products exist in the CRM system
  2. Verify the status parameter (defaults to active)
  3. Contact TBC support to confirm products are published

Pagination Loop (Importing Same Products Repeatedly)

Section titled “Pagination Loop (Importing Same Products Repeatedly)”

Problem: WPGetAPI keeps importing the same products over and over, eventually crashing WordPress with a 503 error.

Root Causes (in order of likelihood):

  1. syncToken not found in response: WPGetAPI may pass only part of the API response to your pagination filter. If your code only checks $data['syncToken'] (root level), it may miss the token. Always check both $data['syncToken'] and $data['pagination']['syncToken'].
  2. API ID mismatch: If the API Unique ID in your PHP code doesn’t match your WPGetAPI settings, the filter is silently skipped.
  3. Missing URL encoding: The syncToken contains + characters (Base64) that become spaces without urlencode(), causing the API to reject the token.
  4. Wrong filter parameters: The WPGetAPI pagination filters pass $api_id and $endpoint_id as plain strings, and the API response as $data (2nd parameter). If your code treats these as objects (e.g., $api->id), the syncToken will never be extracted.

Solution: Use this pagination filter (checks both token locations):

add_filter('wpgetapi_api_to_posts_pagination_next_value', 'tbc_pagination_key', 10, 4);
function tbc_pagination_key($next_value, $data, $api_id, $endpoint_id) {
if ($api_id === 'tbc-crm-api' && $endpoint_id === 'get-products') {
if (is_string($data)) {
$data = json_decode($data, true);
}
// Check both root level and inside pagination object
$token = $data['syncToken']
?? $data['pagination']['syncToken']
?? null;
if (!$token) return null; // Last page — stop
return urlencode($token);
}
return $next_value;
}

Verification:

  1. Enable WordPress debug logging (WP_DEBUG_LOG)
  2. Run the import and check the log for syncToken messages
  3. You should see a few “fetching next page” entries and one final stop
  4. Check API ID: Ensure the $api_id in your PHP code matches exactly what is in your WPGetAPI settings
  5. Confirm no 503 errors after import completes

Problem: Large product catalog

Solution:

  1. Increase the limit parameter to 100 (maximum)
  2. Run sync during off-peak hours
  3. Consider incremental syncs (only changed products)

  • Store credentials in WordPress options or environment variables
  • Use HTTPS for all API communication
  • Cache access tokens (reuse until shortly before expires_in elapses)
  • Rotate credentials every 90 days
  • Monitor failed authentication attempts
  • Hardcode credentials in theme files
  • Commit credentials to version control (GitHub, etc.)
  • Share credentials via email or Slack
  • Expose credentials in frontend JavaScript
  • Request new tokens on every API call

Email: support@tbc-app.com Response Time: 24-48 hours (business days)

Before contacting support, please have ready:

  • Your Client ID (not the secret!)
  • Error messages or screenshots
  • WordPress & WooCommerce versions
  • Steps to reproduce the issue

Check our API status: https://status.tbc-app.com


  1. Get your API credentials from TBC
  2. Choose integration method (wpgetAPI or custom PHP)
  3. Set up and test your connection
  4. Schedule automatic product syncs
  5. Monitor your first sync for errors
  6. Enjoy automated product updates!

Questions? Contact us at support@tbc-app.com