All documentation · Plugin overview
Hooks and developer reference
Development alpha 0.1.0-alpha.1 · Updated 20 September 2026
Applies to the local 0.1.0-alpha.1 candidate. These are the current source contracts, not a promise that an unpublished alpha API will never change. PHP namespaces retain the legacy spelling HoockSpark; public hooks use hs_po_. Do not rename persistent keys during customization.
How an option reaches an order
The repository selects enabled groups using the authoritative product/variation and visitor. The engine validates configuration, evaluates earlier-field dependencies, normalizes selected values and calculates extra prices. Hidden fields are omitted. Cart adapters re-evaluate against catalog prices instead of trusting amounts submitted by a browser. The checkout adapter writes human-readable item metadata and a private _hs_po_snapshot containing purchased options and their extra price.
Never modify prices in the browser as a substitute for server pricing. Never write directly to hs_po_groups_v1 from a public request. The editor routes enforce capability and nonce checks; the store uses an optimistic revision to prevent lost updates. Internal classes are not a general public persistence API.
PHP extension hooks
The first argument of a filter is its current result. Preserve it for definitions your provider does not handle. Set the accepted argument count explicitly when registering the callback.
| Hook | Arguments in order | Required result or action |
|---|---|---|
hs_po_field_configuration_error | $error, $field | null accepts a custom type; a string identifies a configuration error. The default is unsupported_type. |
hs_po_field_value | $result, $field, $value, $context | Return an array with string value and display; optional extra must be finite and nonnegative. Return ['error' => 'code'] to reject. |
hs_po_render_field | $field, $id, $name | Render the custom control, escaping every dynamic value. Use the supplied ID and input name. This is an action, not an HTML-returning filter. |
hs_po_pricing_configuration_error | $error, $pricing | null accepts the pricing configuration; otherwise return an error code. |
hs_po_pricing_unit_extra | $result, $pricing, $value, $base, $quantity | Finite nonnegative integer/float extra per unit. Replaces the field base surcharge; choice extras remain additive. |
hs_po_condition_configuration_error | $error, $condition, $fields, $index | null accepts compound conditions. Validate backward references and bound the condition count. |
hs_po_condition_visible | $visible, $condition, $values | Exactly true makes the compound field visible. Values are validated selections available so far. |
hs_po_steps_configuration_error | $error, $group | null accepts the group’s steps; otherwise a configuration error code. |
Custom field value callbacks receive a trimmed string. Required empty values are rejected before the callback; optional empty values are omitted. The common schema still validates the field ID, label, required flag and standard price/limits. A custom provider must validate any additional properties it introduces.
$context currently contains server-supplied product_id and group_id when called through Quote. Pure engine calls and unsaved previews may supply an empty context. If your provider needs authoritative context, reject missing context rather than reading IDs from $_POST. The fourth argument is additive: existing providers registered for three arguments continue to work.
$base is the catalog unit price in the catalog tax basis, before Product Options & Add-Ons extras. WooCommerce remains responsible for taxes and displayed totals. Repeated calculation must give the same result for the same inputs; do not increment a previously modified product price.
Example: a validated reference code
the complete provider below is a complete provider example. Place it in a site-specific plugin loaded alongside Product Options & Add-Ons; do not edit the Product Options & Add-Ons package. It accepts letters, digits and hyphens up to 32 characters, normalizes them to uppercase and renders a native text input.
A field definition using the provider is:
array(
'id' => 'reference',
'type' => 'acme_reference',
'label' => 'Project reference',
'required' => true,
'price' => 2,
)At catalog price 20, reference abc-42 becomes ABC-42. Two units cost 44 before tax/cart discount effects. A missing reference, spaces, markup and strings longer than 32 characters are rejected. The example is exercised directly by development/tests/product-options/documented-hook-native.php; it does not write settings.
This registers a provider, not a new editor field-type menu item. The editor must also be extended to create the definition. Avoid creating a field configuration that the frontend can render but the merchant cannot understand or edit.
Editor extension events
Events are dispatched on document. Register listeners before the Free editor initializes. The Pro package uses an explicit script dependency for that ordering.
| Event | Detail | Purpose |
|---|---|---|
hs-po-editor-field | card, field, index, group, input, mark, render, conditionHandled | Add controls to the current field. Mark changed drafts and re-render when structure changes. Set conditionHandled only when replacing condition controls. |
hs-po-editor-group | area, group, input, mark, render | Add group-level controls. |
hs-po-editor-ready | merge(groups) | Add validated group drafts to the editor; merge returns success/failure and does not save them. |
hs-po-condition | condition, values, visible | Frontend compound-condition evaluation. Set visible synchronously. Server validation remains authoritative. |
Use textContent for user-controlled labels and native labeled controls. A client-side condition provider also needs the corresponding server configuration and visibility filters. JavaScript alone cannot authorize a purchase.
REST requests and persistence
Namespace: hs-product-options/v1. Use WordPress cookie authentication with X-WP-Nonce and an account allowed to manage WooCommerce. Do not embed administrator credentials in frontend scripts.
Current routes include groups, lookup and draft preview; Pro adds export and import-preview. Import-preview returns disabled drafts, or requires_mapping for cross-site references. Every mapping destination is validated before draft groups are returned. Import-preview is not a save operation.
Handle revision conflicts by reloading the saved state and preserving the merchant’s draft for review. Do not automatically retry with a new revision and overwrite another editor’s work. Request size limits are enforced before processing.
Protected file integrations
Files are not public media attachments. Product options contain an opaque reference bound to server-side session ownership and the exact product/group/field. The order association is stored separately so checkout retries do not consume a pending cart reference. Order downloads require an authenticated merchant, an order/item nonce, matching item metadata and order permission.
Do not output raw upload references in emails, expose database blobs through your own public endpoint, or treat possession of a reference as permission. The current archive is private database storage; permanent order deletion releases unreferenced files; moving an order to Trash preserves them. Authenticated merchant HTTP download acceptance remains a release gate.
Pro deactivation and integration limits
Without the provider for a Pro field, advanced pricing, compound condition or step layout, schema validation fails closed. Definitions are retained. An affected purchase must not silently discard required customization or revert to a cheaper price. Actual network deactivation/reactivation has passed the Free-purchase and Pro-fail-closed checks without data loss. Complete merchant recovery UI remains a release check; this is not a claim that all Free groups remain purchasable in every mixed-group configuration.
Dynamic Pricing integration uses its hs_pricing_catalog_unit_price filter to supply the freshly calculated base with option extras after prior totals are restored. Do not add the same extras again in another price filter. Min/Max continues to validate Woo cart quantities and amounts. Compatibility claims require tests against exact plugin versions.
Administrative REST response contract
| Request | Success | Errors to handle |
|---|---|---|
GET /groups | Saved store snapshot with revision and groups | 401/403 access denied; 409 incompatible stored schema |
PUT /groups | Saved state after optimistic revision check | 400 malformed revision/groups; 413 body over 1 MB; 422 addressed field errors; 409 conflicting revision |
POST /draft-preview | Markup, validation and calculated preview without saving | Access/product checks and validation errors; never treat preview as persistence |
GET /export (Pro) | Versioned saved configuration document | Access or stored-schema errors |
POST /import-preview (Pro) | Disabled new groups, or requires_mapping | 400 format, 413 size, 422 invalid mapping/group |
PUT replaces the whole group collection, not a single group. Read the current snapshot, preserve unrelated groups, edit a local copy, and submit its original revision with the complete collection. Never send an empty collection as a connection test: a valid authorized request would remove all saved groups. On 422, data.fields maps field paths to error codes; retain the draft and associate messages with those controls. On 409, offer a comparison/reload instead of silently overwriting another editor.
The complete reference provider was rerun against the current local runtime: seven checks pass for schema acceptance, normalized uppercase value, total44, invalid values and renderer naming. The six administrative routes also reject anonymous real HTTP requests (401); this does not establish authenticated nonce or every role combination.
Multipart draft file evaluation
The preview endpoint also accepts multipart form data: a draft JSON string with the ordinary preview payload and uploaded files keyed by field ID. Limits are 1 MiB for JSON, 10 files and 10 MiB aggregate uploaded sizes, plus each field’s policy and PHP/server limits. Authentication and product permissions are unchanged.
Only this endpoint supplies the internal draft_preview/draft_files engine context. The Pro upload provider requires is_uploaded_file() and reuses UploadPolicy. It returns an ephemeral display value for calculation, not a reusable storefront upload reference. Files are not moved to storage, media or order records. Do not accept these context flags from public cart values; custom field integrations must preserve the distinction. Authenticated multipart/browser acceptance verified PNG success, invalid content rejection and recovery on 20 September 2026.
Complete provider example
<?php
/** Example custom field provider for HS Product Options & Add-Ons. */
namespace Acme\ProductOptionsExample;
defined( 'ABSPATH' ) || exit;
add_filter('hs_po_field_configuration_error', static function ($error, array $field) {
return 'acme_reference' === ($field['type'] ?? '') ? null : $error;
}, 10, 2);
add_filter('hs_po_field_value', static function ($result, array $field, string $value, array $context = array()) {
if ('acme_reference' !== ($field['type'] ?? '')) {
return $result;
}
if (!preg_match('/^[A-Za-z0-9-]{1,32}$/D', $value)) {
return array('error' => 'invalid_reference');
}
$normalized = strtoupper($value);
return array('value' => $normalized, 'display' => $normalized);
}, 10, 4);
add_action('hs_po_render_field', static function (array $field, string $id, string $name): void {
if ('acme_reference' !== ($field['type'] ?? '')) {
return;
}
echo '<label for="' . esc_attr($id) . '">' . esc_html($field['label']) . '</label>';
echo '<input type="text" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" maxlength="32" pattern="[A-Za-z0-9-]{1,32}"' . (!empty($field['required']) ? ' required' : '') . '>';
}, 10, 3);