Developer guide
| Add-on ID | kv_meta_conversions_api |
| PHP namespace | Tygh\Addons\KvMetaConversionsApi (app/addons/kv_meta_conversions_api/Tygh/Addons/KvMetaConversionsApi/) |
| Function prefix | fn_kv_meta_conversions_api_ |
| Language variables | kv_meta_conversions_api.* |
| Tables | ?:kv_meta_capi_events, ?:kv_meta_capi_orders, ?:kv_meta_capi_vendors |
Consent contract
Section titled “Consent contract”The add-on implements the Kavio Labs consent contract, which the Kavio Labs Cookie Consent add-on (kv_cookie_consent_mode) provides. Any other banner can use it too.
JavaScript: consent provider side
Section titled “JavaScript: consent provider side”A consent banner exposes window.kvConsent (category names: necessary, preferences, analytics, marketing):
window.kvConsent = { has: function (category) { /* true | false */ }, // alias: get() hasDecision: function () { /* true when the visitor made a valid choice */ }, on: function (event, callback) { /* 'ready' | 'change'; returns an unsubscribe function */ }};and dispatches DOM events on document with the current state in event.detail:
document.dispatchEvent(new CustomEvent('kv:consent-ready', {detail: {necessary: true, preferences: false, analytics: true, marketing: true, decided: true}}));document.dispatchEvent(new CustomEvent('kv:consent-changed', {detail: {/* same shape */}}));kv_consent_changed is accepted as a legacy alias. This add-on reads marketing.
Rules:
- If
window.kvConsentexists when the page loads, it is the only source of the decision. WhilehasDecision()isfalse(first visit, expired choice, new policy version), the pixel does not load and nothing is sent, whatever the add-on’s ownkv_meta_consentcookie says. The add-on subscribes withkvConsent.on('ready' | 'change'). - Without
window.kvConsent, the add-on listens for the DOM events, and for the CS-Cart cookie dialog (GDPR add-on, Klaro servicekv_meta_pixel) when that integration is on. - The consent request to the server is sent only after the page scripts are ready, so it is safe to call
kvConsentcallbacks early in<head>.
JavaScript: direct API
Section titled “JavaScript: direct API”kvMetaCapi.consent(true); // grant marketing consent (custom banners)kvMetaCapi.consent(false); // deny or withdrawkvMetaCapi.state(); // 'granted' | 'denied' | 'unknown'kvMetaCapi.sent; // events sent on this page: [{pixel, name, id, data}]kvMetaCapi.send({name: 'Contact', id: 'my-unique-id', data: {}, main: true, vendors: []}); // browser onlyThe add-on triggers $.ceEvent('trigger', 'kv_meta_capi.consent', [granted]) when the state changes.
PHP: consent provider side
Section titled “PHP: consent provider side”Server-side events need the decision during the storefront request. The add-on asks, in this order:
- The PHP hook
kv_meta_conversions_api_get_consent(see below). Any add-on can decide here. fn_kv_cookie_consent_mode_get_consent(), which returns the visitor’s valid choice ornull(Kavio Labs Cookie Consent). If the function exists it is authoritative:nullmeans “no decision yet”.- A generic
fn_kv_consent_has(string $category): ?bool, if a consent add-on defines it (null= unknown). - Only when no consent add-on is present: the
kv_meta_consentcookie (g/d) that the storefront script writes.
Cookie consent add-on services
Section titled “Cookie consent add-on services”When Kavio Labs Cookie Consent is active, the add-on registers the service kv_meta_conversions_api (category marketing) through the kv_cookie_consent_mode_get_services hook. It lists the server events in the preference center, and the consent add-on deletes _fbp, _fbc and kv_meta_consent when marketing consent is withdrawn. No script is blocked, because the pixel loader waits for consent itself. The configuration script (window.kvMetaCapiConfig) is skipped by the consent add-on’s script blocker.
PHP hooks provided
Section titled “PHP hooks provided”/** * Decide marketing consent for the current visitor. * * @param string $category "marketing" * @param bool|null $granted set true/false; leave null for the default resolution */fn_set_hook('kv_meta_conversions_api_get_consent', $category, $granted);
/** * Change or skip a catalog feed item. * * @param array $product Product (fn_get_products + fn_gather_additional_products_data) * @param array|null $item Feed item (id, title, description, availability, condition, price, sale_price, * link, image_link, additional_image_link, brand, item_group_id, product_type, mpn); * set to null to leave the product out */fn_set_hook('kv_meta_conversions_api_feed_item', $product, $item);Example: send a Google product category with every feed item.
fn_register_hooks('kv_meta_conversions_api_feed_item');
// func.phpfunction fn_my_addon_kv_meta_conversions_api_feed_item($product, &$item){ if (is_array($item)) { $item['product_type'] = 'Apparel & Accessories > Clothing'; }}Core hooks used
Section titled “Core hooks used”| Hook | Purpose |
|---|---|
before_dispatch |
Cart/wishlist snapshot, fbclid capture |
redirect |
AddToCart / AddToWishlist before redirects (delivered with the next response) |
dispatch_before_display |
Page events, AJAX payload (kv_meta_capi key), _fbp/_fbc after consent |
place_order |
Browser context of the order (fbp, fbc, IP, user agent, page, consent); AddPaymentInfo |
change_order_status_post |
Server-side Purchase when the order reaches a purchase status |
update_profile |
CompleteRegistration |
newsletters_update_subscriptions_post |
Lead (newsletter) |
create_call_request_post |
Lead (call request) |
update_company, delete_company |
Vendor pixel fields |
kv_cookie_consent_mode_get_services |
Service registration in the Kavio Labs consent add-on |
before_dispatch (admin area and console only; static handler License\Client::onBeforeDispatch) |
Runs the due license check and shows license notices |
addons:tabs_content (pre, admin template) |
License card at the top of the add-on page |
addons:manage (post, admin template) |
License badge, moved into the add-on’s row (4.17–4.21 have no per-row hook) |
index:styles (post, admin template) |
license.less (kv-license block) |
Templates: index:head_scripts (configuration and fbq stub), index:scripts (runtime script and cookie dialog translations), companies:detailed_content (vendor pixel block). Schemas: gdpr/klaro_config.post.php, menu/menu.post.php, permissions/admin.post.php, permissions/vendor_multivendor.post.php, settings/variants.functions.post.php.
Controllers
Section titled “Controllers”| Dispatch | Area | Purpose |
|---|---|---|
kv_meta_conversions_api.consent (POST) |
storefront | Stores the consent decision, writes _fbp/_fbc, releases or drops held server events |
kv_meta_conversions_api.feed?key=…&format=csv|xml[&vendor_id=N] |
storefront | Catalog feed. Wrong key: HTTP 403 |
kv_meta_conversions_api.process?token=… |
storefront | Background queue worker (hourly HMAC token) |
kv_meta_conversions_api.process |
admin, console | Sends the queue (php admin.php --dispatch=kv_meta_conversions_api.process) |
kv_meta_conversions_api.manage |
admin | Event log |
kv_meta_conversions_api.retry, .clear, .test (POST) |
admin | Log actions and the test event |
kv_meta_conversions_api.save_vendor_pixel (POST) |
admin, vendor panel | Saves the vendor pixel block only |
kv_meta_conversions_api_license.check (POST, console) |
admin, console | License check (“Check now”, cron); generated |
Data model
Section titled “Data model”?:kv_meta_capi_events is the queue and the event log: event_id, event_name, pixel_id, vendor_id (0 = store pixel), company_id/storefront_id (settings context), order_id, payload (the API event JSON, without the token), status (Q, P, R, S, F, X), attempts, next_attempt_at, claim (worker lock), created_at, sent_at, http_status, response and test_event_code.
?:kv_meta_capi_orders stores one row per storefront order: fbp, fbc, client_ip, user_agent, source_url, external_id, consent (G/D/empty), placed_at, rendered_at (browser Purchase printed), queued_at (store pixel Purchase queued) and vendor_queued_at.
?:kv_meta_capi_vendors holds company_id, pixel_id, access_token and test_event_code.
Classes
Section titled “Classes”| Class | Responsibility |
|---|---|
Config |
Typed settings, per storefront; Graph API URL and version |
Normalizer |
Meta normalisation and SHA-256 hashing (no CS-Cart dependencies, unit-tested) |
UserData |
Customer data from the profile, checkout or order; IP, user agent, fbp/fbc cookies |
ContentBuilder |
content_ids, contents, value, currency from product lines |
Tracker |
Event creation (browser + server), session queues, cart diff, page events |
OrderTracker |
Order context, browser and server Purchase, Multi-Vendor split |
Queue |
Queue, async delivery, retry/backoff, batching, purge |
GraphApi |
POST /{version}/{pixel}/events, response handling, log masking |
Consent |
Consent mode and the decision resolution described above |
CatalogFeed |
CSV / XML feed |
VendorPixels |
Vendor pixel storage |
ConflictDetector |
Other Meta pixel add-ons |
License\Client |
License check client (generated from the shared Kavio Labs template); admin templates license/{info,card,status_bar,badge}.tpl, license.less |
License check
Section titled “License check”Tygh\Addons\KvMetaConversionsApi\License\Client verifies the license against the Kavio Labs license server. The client, the “Check now”/cron controller controllers/backend/kv_meta_conversions_api_license.php and the License tab template design/backend/templates/addons/kv_meta_conversions_api/license/info.tpl are generated from the shared Kavio Labs template; don’t edit them by hand.
- Admin license UI (generated, REVISION 3; don’t edit by hand):
design/backend/templates/addons/kv_meta_conversions_api/license/card.tpl(License card at the top of the add-on page, every tab;hooks/addons/tabs_content.pre.tpl),license/badge.tpl(badge in Add-ons > Manage add-ons;hooks/addons/manage.post.tpl),license/status_bar.tpl(chip/banner for the add-on’s own admin pages),design/backend/css/addons/kv_meta_conversions_api/license.less(loaded by the kv-license block ofhooks/index/styles.post.tpl). The status bar is the first line inside the mainbox capture ofviews/kv_meta_conversions_api/manage.tpl(event log). All of them render nothing for vendors and for administrators without the “Manage settings” privilege. The “what is switched off” sentence is the language variablekv_meta_conversions_api.license_off_effect.License\Clientassigns the view data to$kv_lic_kv_meta_conversions_apiin itsbefore_dispatchhandler (only for administrators with the Manage settings privilege); the templates only read this variable, so they render nothing (and no error) while an older version of the PHP code is loaded, for example in the request that uploads a new zip over the installed add-on. Templates never call add-on functions; PHP code can callfn_kv_meta_conversions_api_license_ui($context). fn_kv_meta_conversions_api_is_licensed($storefront_id = 0)(func.php) is the gate. It reads the stored state only and never contacts the server. With a storefront ID in the admin area it checks that storefront’s host.Tracker::isTrackingActive()returnsfalse: nokv_meta_capi_config_json(no pixel), no browser or server events, no order context.- Frontend controller:
consentandrelayanswer 204 without recording anything,feedanswers 404. OrderTracker::onStatusChange()queues no Purchase for an order of an unlicensed storefront;Queue::process()sends nothing; the admin Send a test event (.test) refuses with a notice.Client::boot()(init.php) registers abefore_dispatchhandler for the admin area and the console only. On admin page views and console requests it runs the check when it is due (key changed, or 24 hours after the last check; 1 hour after a failure) and shows the admin notice once per session.- Request:
POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.checkwith JSON{"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent throughTygh\Http(3 s connect, 5 s total; kept out of the CS-Cart request log). The answer is accepted only when its Ed25519 signature matches a public key embedded in the client and it echoes the nonce. - State: storage key
kv_license_kv_meta_conversions_api(?:storage_data), orvar/kv_storage/kv_meta_conversions_api/license.jsonwhile the full page cache is active, protected by an HMAC with the store’s crypt key. - Cron:
php admin.php --dispatch=kv_meta_conversions_api_license.check. - Development stores only (
http_hostandhttps_hostare development hosts): the constantsKV_LICENSE_SERVER_URL,KV_LICENSE_TEST_HOST(act as if the development storefronts ran on that host) andKV_LICENSE_TEST_TIME_OFFSET(seconds). They are ignored on production hosts.
Delivery details
Section titled “Delivery details”Storefront requests only insert queue rows. In automatic mode, a shutdown function then either flushes the response with fastcgi_finish_request() / litespeed_finish_request() and sends up to 100 events, or opens a non-blocking connection to kv_meta_conversions_api.process. That request is fired and forgotten, at most once every 2 seconds. Workers claim rows with UPDATE … SET claim = … LIMIT n, so parallel workers never send an event twice. Events are grouped per pixel, token and test code, and sent in batches of up to 500. The access token travels in the JSON body, never in the URL, and is masked in CS-Cart’s HTTP request log.
Local testing without Meta
Section titled “Local testing without Meta”tests/mock/router.php is a mock of the Conversions API endpoint. It validates the payload (hash format, required fields, time window) and records every request. Run it with PHP’s built-in server and point the add-on at it in development only:
// local_conf.php or config.local.php of a development storedefine('KV_META_CONVERSIONS_API_GRAPH_URL', 'http://127.0.0.1:8197');The settings page shows a warning while the constant is defined. Unit tests for normalisation and hashing: php tests/unit/run.php.
Third-party code
Section titled “Third-party code”Tygh/Addons/KvMetaConversionsApi/License/Ed25519.php (checks the signature of the license server’s answers on servers whose PHP has no sodium extension) is derived from paragonie/sodium_compat, ISC License, Copyright (c) 2016-2023 Paragon Initiative Enterprises and Copyright (c) 2013-2019 Frank Denis. The full license notice is in the file header.