All documentation · Plugin overview

Hooks and developer reference

Documentation for version 1.0.4.

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.

HookArguments in orderRequired result or action
hs_po_field_configuration_error$error, $fieldnull accepts a custom type; a string identifies a configuration error. The default is unsupported_type.
hs_po_field_value$result, $field, $value, $contextReturn 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, $nameRender 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, $pricingnull accepts the pricing configuration; otherwise return an error code.
hs_po_pricing_unit_extra$result, $pricing, $value, $base, $quantityFinite nonnegative integer/float extra per unit. Replaces the field base surcharge; choice extras remain additive.
hs_po_condition_configuration_error$error, $condition, $fields, $indexnull accepts compound conditions. Validate backward references and bound the condition count.
hs_po_condition_visible$visible, $condition, $valuesExactly true makes the compound field visible. Values are validated selections available so far.
hs_po_steps_configuration_error$error, $groupnull 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.

EventDetailPurpose
hs-po-editor-fieldcard, field, index, group, input, mark, render, conditionHandledAdd controls to the current field. Mark changed drafts and re-render when structure changes. Set conditionHandled only when replacing condition controls.
hs-po-editor-grouparea, group, input, mark, renderAdd group-level controls.
hs-po-editor-readymerge(groups)Add validated group drafts to the editor; merge returns success/failure and does not save them.
hs-po-conditioncondition, values, visibleFrontend 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. Merchant downloads require order permissions and a valid item association; private references are not public media URLs.

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

RequestSuccessErrors to handle
GET /groupsSaved store snapshot with revision and groups401/403 access denied; 409 incompatible stored schema
PUT /groupsSaved state after optimistic revision check400 malformed revision/groups; 413 body over 1 MB; 422 addressed field errors; 409 conflicting revision
POST /draft-previewMarkup, validation and calculated preview without savingAccess/product checks and validation errors; never treat preview as persistence
GET /export (Pro)Versioned saved configuration documentAccess or stored-schema errors
POST /import-preview (Pro)Disabled new groups, or requires_mapping400 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);

Handbook index · Try the examples →

Experimental additions in alpha.18

These hooks are additive, available with WordPress loaded, and not a stable cross-version API yet:

  • hs_po_option_snapshot(array $item, array $field): array: runs for each validated, visible selected field during quote calculation. Preserve required keys and calculated money; Pro adds the optional sku. No capability check here: quotes also run for storefront customers. Never read privileged data or trust unsanitized input in this filter.
  • hs_po_field_metadata_error($error, array $field): schema validation extension; return the previous error or an error code. Pro validates optional field/choice codes using invalid_option_sku.
  • hs_po_admin_tabs(array $tabs): array: adds a tab slug/label to the existing admin navigation whitelist.
  • hs_po_admin_tab(string $tab): void: renders extension tab content. Extensions must check their own capability and escape output; Pro reports require manage_woocommerce.

Order item _hs_po_snapshot schema2 adds base, quantity, currency, catalog_includes_tax; item entries add pricing_mode, once_extra and optional Pro sku. Keep schema1 reads compatible. This metadata is an immutable purchase snapshot, not a supported direct-write integration endpoint. Historical orders are not migrated.

The Woo Store API batch adapter keeps the existing contract: each subrequest supplies extensions.hs-product-options.values keyed by its own group IDs. Native REST tests exercise two different products and a mixed valid/invalid batch: overall207, valid201, invalid4xx, with only the valid configuration added. This does not establish atomic all-or-nothing semantics or replace HTTP session/nonce testing.

Cart item_data values in alpha.18 include escaped HTML within a hs-po-cart-value span for scoped presentation in Woo Classic/Blocks. Consumers requiring plain text should strip markup. Stored order display metadata remains plain text.

Expansion hooks — local candidate contracts

These additions are not a stable public API release. Register callbacks only when the relevant plugin is available, retain incoming values when your callback does not apply, and never trust posted prices or media IDs.

hs_po_pricing_configuration_error

The existing filter now receives an optional third argument: the owning field definition. Existing two-argument callbacks remain compatible. Arguments are the previous error or null, pricing array, and field array. Return null only for a supported and valid definition; otherwise return an error code. Pro customer_amount requires a Number field and finite nonnegative minimum and maximum values, with maximum not below minimum.

hs_po_cart_line_quote

Arguments: authoritative quote array, cart key, native item array, and WC_Cart. Return a complete quote array. The linked-product adapter removes accessory charges from the main line because WooCommerce bills native child lines separately. Do not remove validation errors or duplicate accessory charges in a downstream integration.

hs_po_cart_product

Arguments: isolated WC_Product clone for this line and authoritative quote array. Return a WC_Product. The selected-photo adapter applies the validated snapshot's attachment reference to this clone. Never save this object: it represents a cart line, not a catalog update. Avoid mutating shared catalog objects or assuming the quote has no errors.

hs_po_option_snapshot

The existing snapshot filter also supports product_image_id populated from the merchant's validated visual choices. This reference is copied into the order's private _hs_po_snapshot metadata with the economic components. It is not accepted as a trusted shopper parameter. Consumers must tolerate older snapshots without this property and attachments that no longer exist.

Display-only fields and group previews

hs_po_field_is_display_only receives the previous boolean and field definition. True excludes the editorial field from value/price evaluation. Do not use it to suppress validation for a purchased input.

hs_po_render_group_preview is an action receiving the validated group and group ID. Escape output for its HTML context. Pro uses it for bounded text/image layers. hs_po_composite_configuration_error validates the optional composite configuration; absent support must remain an error rather than silently ignoring an unsupported definition.

Cart edit transport

The Pro wc-ajax=hs_po_edit mutation accepts POST only and uses the current Woo session. It requires the existing cart key, a session-and-line-state-bound nonce, and nested values. Omitting values means clearing all optional choices, not bypassing required fields. The server recalculates from saved groups/catalog data and rejects direct child-line edits.

A stale nonce, unknown line, invalid method or another session cannot select a different customer's cart. The nonce includes quantity and selections; it is not a database lock and does not by itself serialize simultaneous Woo session writes. Invalid configuration returns localized field messages when available and preserves the prior cart. Do not implement automatic POST retries after an ambiguous network failure; reload the cart first.

Modal availability

hs_po_modal_available(bool $available): bool defaults to false. The installed Pro provider enables it independently of renewal entitlement. The server renderer uses inline markup when unavailable, even if a stored definition requests modal presentation. Stored definitions remain structurally valid and unchanged, preserving reactivation behavior. This is a presentation fallback only; unsupported field/pricing providers still fail validation.

The frontend dispatches hs-po-refresh on document before recalculating visible fields, with detail.container and detail.refresh. Pro registers its modal presentation controller on this event. Extensions must avoid synchronously calling refresh during every notification, which would recurse. This local candidate event is not a stable public API.

Input normalization

hs_po_normalize_field_input($value, array $field) receives the submitted value and field definition before scalar conversion, trimming and required checks. Preserve values for field types your provider does not own. Return a string, integer or float; other return types are rejected as invalid_value. Pro uses this boundary for illustrated multiple-choice values. Normalization is not authorization: the provider must still reject unknown choices, enforce selection limits and calculate prices on the server. Do not flatten arbitrary input arrays for unrelated field types.

Display amount integration

hs_po_quote_display_amounts(array $amounts, array $quote, WC_Product $product, int $quantity): array runs after the standard WooCommerce display-tax conversion. Preserve the base, extras and total numeric keys and the applicable tax-display basis. The linked-product provider adds accessories using each accessory's own tax class. These numbers describe the selected configuration before cart discounts and shipping; modifying them does not change authoritative cart charges. Keep this filter consistent with the server quote and never use it to conceal or duplicate a charge.

Validation response — version1.0.1

Invalid selections at the add-to-cart boundary return HTTP400 with code hs_product_options_invalid_selection through WooCommerce Store API. This includes missing required values, malformed selection input and invalid field values. A rejected subrequest does not add a cart line. The batch endpoint retains WooCommerce’s per-subrequest semantics: overall207, valid201 and invalid400, with only valid configurations added; this is not an atomic transaction across the batch.

The payload remains extensions.hs-product-options.values. Classic WooCommerce additions retain the customer error notice and failed add result. Repository/storage failures are not mislabeled as customer validation errors. No new public HookSpark hook or data migration is introduced.