WooCommerce Integration Guide
Welcome! This guide will help you integrate your WooCommerce store with the TBC CRM product catalog.
Quick Start
Section titled “Quick Start”What You’ll Need
Section titled “What You’ll Need”-
API Credentials (provided by TBC team)
- Client ID
- Client Secret
- Allowed scopes
-
WordPress/WooCommerce Site
- WordPress 5.0+
- WooCommerce plugin installed
- wpgetAPI plugin (recommended)
-
Integration Time
- Basic setup: 30 minutes
- Full product sync: 1-2 hours
Step 1: Get Your Credentials
Section titled “Step 1: Get Your Credentials”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.readStep 2: Choose Your Integration Method
Section titled “Step 2: Choose Your Integration Method”Option A: Using wpgetAPI Plugin (Recommended)
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
Option B: Custom PHP Code
Section titled “Option B: Custom PHP Code”Best for: Developers, custom requirements
Pros:
- Full control over sync logic
- Customizable product mapping
- Advanced error handling
- Schedule flexibility
Setup Time: 1-2 hours
Option A: wpgetAPI Plugin Setup
Section titled “Option A: wpgetAPI Plugin Setup”1. Install wpgetAPI
Section titled “1. Install wpgetAPI”- In WordPress admin, go to Plugins > Add New
- Search for “wpgetAPI”
- Click Install Now, then Activate
2. Configure API Connection
Section titled “2. Configure API Connection”- Navigate to wpgetAPI > Add New
- Fill in the following:
Basic Settings:
API Name: TBC CRM APIBase URL: https://{api-base-url}/Authentication:
Auth Type: OAuth 2.0Grant Type: Client CredentialsToken URL: {token-endpoint}Client ID: [Your Client ID]Client Secret: [Your Client Secret]Scope: crm-api/products.readAdvanced Settings:
Token Caching: Enabled (recommended)Cache Duration: 3000 seconds (50 minutes; new clients get 1-hour tokens)- Click Save
3. Add Product Endpoint
Section titled “3. Add Product Endpoint”- In the same API settings, scroll to Endpoints
- Click Add Endpoint
- Configure:
Endpoint ID: get-productsMethod: GETEndpoint Path: products/searchQuery String: size=50&updatedAfter=1970-01-01T00:00:00Z- Click Save
4. Test Connection
Section titled “4. Test Connection”- Click Test Endpoint next to
get-products - You should see a JSON response with product data
- If you see an error, verify your credentials and try again
5. Sync Products
Section titled “5. Sync Products”Add this code to your theme’s functions.php or create a custom plugin:
<?phpfunction 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 AMif (!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.
syncTokenpaging 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 byid). 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.
- Install the API to Posts extension.
- Configure mapping (e.g.,
productCode->_sku,price->_regular_price). - 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 dataadd_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;}Option B: Custom PHP Setup
Section titled “Option B: Custom PHP Setup”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:
<?phprequire_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
Efficient Syncing (Incremental Updates)
Section titled “Efficient Syncing (Incremental Updates)”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.
1. Configure Endpoint in wpgetAPI
Section titled “1. Configure Endpoint in wpgetAPI”Add a new endpoint in wpgetAPI:
Endpoint ID: get-updatesMethod: GETEndpoint Path: products/searchQuery String: size=502. Implement Incremental Sync Logic
Section titled “2. Implement Incremental Sync Logic”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');API Reference
Section titled “API Reference”Endpoints You Can Use
Section titled “Endpoints You Can Use”Search Products (Recommended)
Section titled “Search Products (Recommended)”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:truewhen more pages exist,falseon the last pagesyncToken: pass this as a query parameter to fetch the next page.nullon the last page.
Price field semantics:
| Field | Description | WooCommerce mapping |
|---|---|---|
price | Trade/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 |
retailPrice | Recommended retail price (RRP) — what end-consumers pay on the storefront | Map 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 Single Product
Section titled “Get Single Product”GET /products/{id}Authorization: Bearer {access_token}Response:
{ "id": "prod_abc123", "productCode": "CH-BM-001", "name": "Chrome Basin Mixer", "price": 299.99, ...}Rate Limits
Section titled “Rate Limits”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=100and 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
sizeparameter (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.
Troubleshooting
Section titled “Troubleshooting””401 Unauthorized” Error
Section titled “”401 Unauthorized” Error”Problem: Invalid or expired access token
Solution:
- Verify Client ID and Client Secret are correct
- Check that credentials haven’t been revoked
- Ensure scope is
crm-api/products.read - Clear your token cache and try again
”403 Forbidden” Error
Section titled “”403 Forbidden” Error”Problem: Insufficient permissions
Solution:
- Verify your scope includes
crm-api/products.read - Contact TBC support to confirm your access level
No Products Returned
Section titled “No Products Returned”Problem: Empty product list
Solution:
- Check if products exist in the CRM system
- Verify the
statusparameter (defaults toactive) - 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):
- 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']. - API ID mismatch: If the API Unique ID in your PHP code doesn’t match your WPGetAPI settings, the filter is silently skipped.
- Missing URL encoding: The
syncTokencontains+characters (Base64) that become spaces withouturlencode(), causing the API to reject the token. - Wrong filter parameters: The WPGetAPI pagination filters pass
$api_idand$endpoint_idas 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:
- Enable WordPress debug logging (
WP_DEBUG_LOG) - Run the import and check the log for
syncTokenmessages - You should see a few “fetching next page” entries and one final stop
- Check API ID: Ensure the
$api_idin your PHP code matches exactly what is in your WPGetAPI settings - Confirm no 503 errors after import completes
Sync is Slow
Section titled “Sync is Slow”Problem: Large product catalog
Solution:
- Increase the
limitparameter to 100 (maximum) - Run sync during off-peak hours
- Consider incremental syncs (only changed products)
Security Best Practices
Section titled “Security Best Practices”- Store credentials in WordPress options or environment variables
- Use HTTPS for all API communication
- Cache access tokens (reuse until shortly before
expires_inelapses) - Rotate credentials every 90 days
- Monitor failed authentication attempts
DON’T:
Section titled “DON’T:”- 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
Support
Section titled “Support”Documentation
Section titled “Documentation”- API Reference: https://docs.tbc-app.com/api
- WooCommerce Guide: https://docs.tbc-app.com/integrations/woocommerce
- FAQ: https://docs.tbc-app.com/faq
Need Help?
Section titled “Need Help?”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
Service Status
Section titled “Service Status”Check our API status: https://status.tbc-app.com
What’s Next?
Section titled “What’s Next?”- Get your API credentials from TBC
- Choose integration method (wpgetAPI or custom PHP)
- Set up and test your connection
- Schedule automatic product syncs
- Monitor your first sync for errors
- Enjoy automated product updates!
Questions? Contact us at support@tbc-app.com