REST API Reference
{/* AUTO-GENERATED from ../docs/rest-api-reference.md by scripts/sync-dev-docs.mjs — do not edit by hand. */}
Dino Discounts exposes admin REST routes under /wp-json/dino-discounts/v1/, registered by the per-resource controllers in includes/API/ (wired up by RestAPI::register_routes()). It also attaches cart data to the WooCommerce Store API under extensions["dino-discounts"]. This page lists every registered route. The rule wire shape itself is contracted by the live JSON Schema served at GET /wp-json/dino-discounts/v1/schema — when this prose and the schema disagree, the schema wins.
- Version:
v1(baked into the namespace — there is no/v2) - Base URL:
/wp-json/dino-discounts/v1 - Content type: All request bodies and responses are JSON unless an endpoint declares otherwise (CSV / ZIP / TXT exports).
- Related reading: Architecture · Hooks reference
Contents
Section titled “Contents”- Authentication
- Conventions
- Schema —
/schema(live JSON Schema for rules) - Rules —
/rules,/rules/diff-summary,/rules/draft,/rules/reset-to-seeded - Usage counters —
/rules/{rule_id}/usage/reset,/rules/{rule_id}/per-customer-uses/reset,/usage/reset-all - Global settings —
/global-settings,/settings/*,/zones/{id},/telemetry-consent - Rule snapshots —
/snapshots - Cart preview —
/preview - Cart scenarios —
/cart-scenarios - Coupons —
/coupons - Analytics —
/analytics/* - Product & taxonomy search —
/products/search,/products/resolve-skus,/taxonomy/search - Diagnostics —
/performance/log,/cache-compat,/bug-report/*,/telemetry/error - Compatibility Center —
/compat - Demo store —
/demo-store/seed - Onboarding —
/onboarding - Store API extension —
wc/store/v1/cartextensions.dino-discounts - Error shape
Authentication
Section titled “Authentication”Almost every route registers AbstractController::check_permission() as its permission_callback, which requires the manage_woocommerce capability. The exceptions:
| Route | Permission |
|---|---|
POST /telemetry-consent | manage_options |
POST /demo-store/seed | manage_options |
POST /bug-report/submit | Logged in, plus edit_posts (filterable via dino_discounts_bug_report_capability). Rate limited per user; manage_woocommerce users are exempt from the limit. |
POST /compat/{id}/probe | Public (__return_true). Called by the storefront probe asset; accepts only a scoped probe token (X-Dino-Compat-Probe header) and one enumerated outcome. See Compatibility Center. |
Under-privileged requests get the standard WordPress errors (see below).
Supported auth methods
Section titled “Supported auth methods”| Method | Header / param | Notes |
|---|---|---|
| Logged-in cookie + nonce | X-WP-Nonce: <nonce> | The canonical admin path. The plugin localises a fresh wp_rest nonce into window.dinoDiscountsAdmin.nonce (includes/Admin.php). |
| Application Password | Authorization: Basic <base64(user:app_password)> | WordPress built-in. Works on HTTPS. |
| Basic Auth plugin / JWT / OAuth1 | Per plugin | Not bundled — use if you have an existing setup. |
The plugin does not add custom CORS headers. If you need to call these endpoints from another origin, configure CORS at the WordPress layer (rest_pre_serve_request) or a reverse proxy.
Getting a nonce from inside WP admin
Section titled “Getting a nonce from inside WP admin”$nonce = wp_create_nonce( 'wp_rest' );From JavaScript inside the admin app, use window.dinoDiscountsAdmin.nonce.
Common auth failures
Section titled “Common auth failures”| Status | Error code | Meaning |
|---|---|---|
| 401 | rest_forbidden | Not logged in (no cookie + nonce, no application password). |
| 403 | rest_forbidden | Logged in but missing the route’s capability. |
| 403 | rest_cookie_invalid_nonce | Nonce header stale (WP default TTL is 24h via the nonce_life filter) or bound to a different user. Fetch a fresh nonce. |
Conventions
Section titled “Conventions”Request bodies
Section titled “Request bodies”JSON only. Set Content-Type: application/json on every POST/PUT/PATCH/DELETE that carries a body.
Response envelope
Section titled “Response envelope”There is no single envelope — each route returns a resource-shaped object. Mutating routes typically return { "success": true, ... } plus the updated resource; read routes return the resource directly.
Argument validation
Section titled “Argument validation”Several routes declare args with a validate_callback. When one fails, WordPress rejects the request before the handler runs with rest_invalid_param (400); a missing required arg is rest_missing_callback_param (400).
Pagination
Section titled “Pagination”Only a few routes paginate:
/analytics/discounts—page(default 1),per_page(default 25, max 100); response carries apaginationobject./products/search—page(default 1, max 1000),per_page(default 20, max 100); response carries top-leveltotal/totalPages./coupons/find—offset, 50 results per page./coupons/{coupon}/history/{batch_id}/codes—page,per_page(default 50, max 200); response carriestotal/total_pages.
GET /snapshots returns the 20 most recent snapshots. Other list routes (scenarios, coupons, etc.) return the full collection in one response.
Limits and rate limiting
Section titled “Limits and rate limiting”There is no general rate limit. Specific caps:
- Rule saves are capped at 200 rules (
too_many_rules). A plan may also cap the number of active rules (active_rule_cap_reached). - Bulk coupon generation is capped at 10,000 codes per request.
/previewaccepts at most 500 cart rows.POST /bug-report/submitis rate limited per user (429 withRetry-After).POST /compat/{id}/probeis throttled per entry and outcome (429).
Dates and timezones
Section titled “Dates and timezones”Date parameters use YYYY-MM-DD. Analytics date ranges are interpreted against the store timezone (DateTimeHelper::store_now()); the resolved timezone is echoed back in the response under date_range.timezone or timezone.
Rule wire shape
Section titled “Rule wire shape”Every rule sent to or returned by /rules, /rules/diff-summary, /rules/draft, /snapshots/*, and /preview follows the section-shaped rule contract:
- Header:
id,name,is_active(required);exclusive,archived,allow_removal,promotion_id(optional — the sanitiser fills defaultsfalse,false,true,null). - Seven required section objects:
who,when,where,trigger,rewards,messaging,metadata. - Inside
who,when,whereandtrigger, every listed key is required; an unconstrained field is sent asnull, not omitted. A rule missing a required key fails validation. The exceptions aretrigger.cart_total_qty_thresholdandtrigger.pools, which are optional. idis a UUID v4 string (the admin app mints it client-side).nullor0is accepted for a new rule; a legacy positive integer id still round-trips.- There is no top-level
targeting_tree, notypediscriminator (userewards.kind), and nopriorityfield — priority is the index of the rule in the array;POST /ruleswrites rules in display / evaluation order. rewards.kindis one oftiered,bulk,pooled_match.
The admin app posts rules as a node tree (promotion nodes with child rules); a plain flat array of rules is also accepted. See the Schema endpoint for the authoritative shape.
Schema
Section titled “Schema”The live JSON Schema for rules is served by a dedicated read-only endpoint. Use it as the wire contract — client validation, codegen, and integration tests should target this endpoint rather than scrape examples from this page.
GET /schema
Section titled “GET /schema”Return the top-level rule JSON Schema (Draft-07) composed from the per-section fragments in includes/Engine/Schema/ (RuleSchemaValidator::rule_schema() — the same schema the save path validates against). Read-only.
Response — 200 OK
{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://dino-discounts/schemas/rule.json", "title": "Dino Discounts rule", "type": "object", "additionalProperties": false, "required": ["id", "name", "is_active", "who", "when", "where", "trigger", "rewards", "messaging", "metadata"], "properties": { "id": { "oneOf": [ /* null | integer >= 0 | UUID v4 string */ ] }, "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "is_active": { "type": "boolean" }, "exclusive": { "type": "boolean" }, "archived": { "type": "boolean" }, "allow_removal": { "type": "boolean" }, "promotion_id": { "oneOf": [ /* null | UUID v4 string */ ] }, "who": { /* WhoSchema::definition() */ }, "when": { /* WhenSchema::definition() */ }, "where": { /* WhereSchema::definition() */ }, "trigger": { /* TriggerSchema::definition() */ }, "rewards": { /* RewardsSchema::definition() */ }, "messaging": { /* MessagingSchema::definition() */ }, "metadata": { /* MetadataSchema::definition() */ } }}The per-section sub-schemas are served inline — fetch the endpoint once and validate against the resulting document with any standard Draft-07 validator (Ajv, jsonschema, opis/json-schema, …). Passing the schema is necessary but not sufficient: the save path also applies sanitiser checks (for example strict tier ordering and message length caps) and the rule limits above. The admin app’s JS mirror of the schema is src/contracts/ruleJsonSchema.js.
Manage the active discount rule set and its in-progress draft.
GET /rules
Section titled “GET /rules”List the currently-active rules and any saved draft.
Response — 200 OK
{ // `rules[]` is in evaluation / display priority order — index 0 fires first. "rules": [ { "id": "3f2b8c1e-5d4a-4b6f-9a1e-7c2d0e8f4a11", "name": "10% off Summer", "is_active": true, "exclusive": false, "archived": false, "allow_removal": true, "promotion_id": null,
"who": { "roles": null, "is_logged_in": null, "first_time": null, "min_orders": null, "total_spent": null, "days_since_last_order": null }, "when": { "schedule": { "start": "2026-06-01 00:00:00", "end": "2026-08-31 23:59:59" }, "weekdays": null, "time_of_day": null }, "where": { "zones": { "match": "any", "values": ["uk", "eu"] }, "currency": { "match": "any", "values": ["GBP", "EUR"] } }, "trigger": { "spend_threshold": { "operator": "gte", "value": 50 }, "cart_total_qty_threshold": null, "cart_line_count": null, "cart_weight": null, "coupon_ids": null, "wc_coupon_stacking": "allow", "usage_limits": null }, "rewards": { "kind": "tiered", "metric": "spend", "tiers": [ { "threshold": 50, "reward": { "kind": "percent_off", "value": 10 } } ], "reward_target": { "target": "cart" } }, "messaging": { "cart_label": "Summer 10%", "cart_notice": null, "nudge": null, "storefront": null, "eligibility": null }, "metadata": { "recipe_intent": null } } ], "node_tree": [ /* the same rules as a node tree, with promotion nodes */ ], "usage_counts": { "3f2b8c1e-5d4a-4b6f-9a1e-7c2d0e8f4a11": 42 }, "plan": { /* active-rule cap indicator: active, cap, unlimited, ... */ }, "draft": { "rules": [...], "global_settings": {...} }, "draft_tree": [...]}Array order is evaluation / display priority — index 0 is highest. There is no separate priority field. draft and draft_tree are omitted if no draft is saved. usage_counts maps rule id to store-wide redemption count.
Money values in trigger.spend_threshold and spend-tier thresholds are plain decimals in the cart’s currency (no currency key); where.currency is the only currency constraint.
Every field shown above maps to the live /schema document — the example is illustrative, not an alternative contract. Other shapes (rewards.kind: "bulk" | "pooled_match", trigger.pools item pools, rewards.reward_target: { "target": "pools", ... }, trigger.coupon_ids: ["BF20"]) follow the same rules — fetch the schema for the full enum and constraint set.
Messaging placement is store-wide. Where the cart label, spend-more nudge and product-page promo appear is set in Message Settings (the message_settings object on /global-settings), not per rule. The rule schema no longer has the retired per-rule keys messaging.cart_label_visible, messaging.nudge.position_* and messaging.storefront.position. A payload that still carries them has them dropped before validation, so it is accepted and they have no effect. Per rule, messaging.nudge carries one shared pre_trigger.message plus participate (set false to opt the rule out of spend-more nudges).
POST /rules
Section titled “POST /rules”Overwrite the active rule set. Rules that fail schema validation are skipped and reported; the valid subset is published. An audit snapshot is written; if the snapshot write fails, the save is rolled back. Coupons whose last referencing rule was removed by this publish are archived.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
rules | array | ✅ | Section-shaped rules or a node tree (see Rule wire shape and the /schema endpoint). Array order is the evaluation priority — index 0 fires first. Max 200. |
snapshot_name | string | — | Optional name for the audit snapshot. |
comment | string | — | Optional free-text comment attached to the snapshot. |
Response — 200 OK
{ "success": true, "rules": [ /* sanitised rules — same shape as GET /rules */ ], "skipped": [ { "rule_index": 2, "name": "Draft rule", "errors": [ { "path": "...", "message": "..." } ] } ], "archived_coupons": [ "SUMMER10" ], "plan": { /* active-rule cap indicator after the write */ }}Errors
| Status | Code | When |
|---|---|---|
| 400 | rest_missing_callback_param / rest_invalid_param | rules is missing, not an array, or has more than 400 entries. |
| 400 | too_many_rules | More than 200 rules supplied. |
| 400 | unknown_rule_field | The payload is structurally malformed (for example an unknown key). Nothing is saved. |
| 400 | rule.schema_invalid | Every rule failed validation, so nothing could be published. data.errors carries the first failing rule’s { path, message } rows; data.skipped lists every skipped rule. |
| 400 | active_rule_cap_reached | The publish would raise the number of active rules past the plan’s cap. data carries cap, active, incoming. |
| 500 | snapshot_failed | The audit snapshot could not be written; the save was rolled back. |
Example
curl -X POST "https://example.com/wp-json/dino-discounts/v1/rules" \ -H "X-WP-Nonce: $NONCE" \ -H "Content-Type: application/json" \ --cookie "$WP_COOKIE" \ --data @- <<'JSON'{ "rules": [{ "id": "3f2b8c1e-5d4a-4b6f-9a1e-7c2d0e8f4a11", "name": "10% off Summer", "is_active": true, "who": { "roles": null, "is_logged_in": null, "first_time": null, "min_orders": null, "total_spent": null, "days_since_last_order": null }, "when": { "schedule": { "start": "2026-06-01 00:00:00", "end": "2026-08-31 23:59:59" }, "weekdays": null, "time_of_day": null }, "where": { "zones": null, "currency": null }, "trigger": { "spend_threshold": { "operator": "gte", "value": 50 }, "cart_line_count": null, "cart_weight": null, "coupon_ids": null, "wc_coupon_stacking": "allow", "usage_limits": null }, "rewards": { "kind": "tiered", "metric": "spend", "tiers": [ { "threshold": 50, "reward": { "kind": "percent_off", "value": 10 } } ], "reward_target": { "target": "cart" } }, "messaging": { "cart_label": "Summer 10%" }, "metadata": { "recipe_intent": null } }]}JSONPOST /rules replaces the whole active set — send every rule you want to keep.
Fires: dino_discounts_rules_saved (and dino_discounts_first_rule_published on a store’s first publish), then dino_discounts_cache_flush.
POST /rules/diff-summary
Section titled “POST /rules/diff-summary”Compare a proposed rule set against the currently-saved rules without saving. Used by the Publish confirmation.
Request body: same rules array as POST /rules.
Response — 200 OK
{ "diff_summary": "Added: Bulk Discount. Modified: 10% off Summer.", "skipped_count": 0, "coupons_to_archive": [ "FALL20" ]}diff_summary is a human-readable string. skipped_count is how many rules would be skipped as incomplete; coupons_to_archive lists coupons the publish would archive.
Errors: too_many_rules, unknown_rule_field, rule.schema_invalid (all 400).
PUT /rules/draft
Section titled “PUT /rules/draft”Save an in-progress rule edit session (rules + global_settings) so it survives page reloads. Does not affect the live rules. Tier order is checked leniently here (strictly on publish).
Request body
| Field | Type | Required |
|---|---|---|
rules | array | ✅ |
global_settings | object | ✅ |
Response — 200 OK
{ "success": true }Errors: too_many_rules, unknown_rule_field, rule.schema_invalid (all 400).
DELETE /rules/draft
Section titled “DELETE /rules/draft”Discard the saved draft.
Response — 200 OK
{ "success": true }POST /rules/reset-to-seeded
Section titled “POST /rules/reset-to-seeded”Replace the active rule set with the plugin’s starter rules. Destructive; an audit snapshot is written so the previous rules can be restored from History.
Response — 200 OK: { "success": true, "rules": [...], "cleanup": {...} }. cleanup reports the coupons archived by the reset, deleted_usage_counters (count, or null if that step failed), analytics_policy: "retained", and any warnings.
Errors: reset_in_progress (409) — another reset is running.
Usage counters
Section titled “Usage counters”Clear the redemption counters that back trigger.usage_limits. Rule ids are UUIDs.
POST /rules/{rule_id}/usage/reset
Section titled “POST /rules/{rule_id}/usage/reset”Clear the store-wide and per-customer counters for one rule.
Response — 200 OK: { "success": true, "rule_id": "...", "store_rows_deleted": 1, "per_customer_rows_deleted": 12 }.
POST /rules/{rule_id}/per-customer-uses/reset
Section titled “POST /rules/{rule_id}/per-customer-uses/reset”Clear one customer’s per-customer counter for a rule.
| Field | Type | Required | Notes |
|---|---|---|---|
user_id | int | one of | Logged-in customer. |
email | string | one of | Guest billing email. |
guest_handling | string | — | match_by_email (default) or treat_as_first_time; only used with email. |
Response — 200 OK: { "success": true, "rule_id": "...", "customer_key": "...", "rows_deleted": 1 }.
Errors: invalid_target (400) — neither or both of user_id / email, or an invalid email.
POST /usage/reset-all
Section titled “POST /usage/reset-all”Clear every rule’s counters.
Response — 200 OK: { "success": true, "store_rows_deleted": 5, "per_customer_rows_deleted": 80 }.
Global settings
Section titled “Global settings”Store-wide discount configuration (stacking mode, discount cap, coupon coexistence, custom zones, performance tracking, Message Settings).
GET /global-settings
Section titled “GET /global-settings”Response — 200 OK
Returns the dino_discounts_global_settings option merged over GlobalSettingsDefaults::DEFAULTS (telemetry consent keys are not included), plus three keys read from their own storage:
message_settings— the store-wide Message Settings:applied_notice,label_visibility(cart/mini/checkout),nudge(enable,slot_mini,slot_cart,slot_checkout, …),product_promo,removal,rejection,discount_links.coupon_precedence—dino_firstorwc_first.coupon_rejection_messages— merchant overrides for coupon rejection copy.
POST /global-settings
Section titled “POST /global-settings”Request body
| Field | Type | Required | Notes |
|---|---|---|---|
settings | object | ✅ | Settings object. Omitting custom_zones preserves the existing zones (a missing key is not treated as “clear the zones”). message_settings, coupon_precedence and coupon_rejection_messages are saved to their own storage when present. Telemetry consent keys are ignored here (use /telemetry-consent). |
Response — 200 OK
{ "success": true, "settings": { /* sanitised settings */ } }Errors: rest_missing_callback_param / rest_invalid_param (400) if settings is missing or not an object.
Fires: dino_discounts_settings_saved, dino_discounts_cache_flush.
GET / POST /settings/coupon-precedence
Section titled “GET / POST /settings/coupon-precedence”Read or set whether Dino or WooCommerce resolves a typed coupon code first. GET returns { "precedence": "dino_first" }; POST takes { "precedence": "dino_first" | "wc_first" } and returns { "success": true, "precedence": "..." }.
GET / POST /settings/engine-hook-priority
Section titled “GET / POST /settings/engine-hook-priority”Read or set the priority of the engine’s cart hook. GET returns { "priority": 20, "default": 20, "min": 16 }; POST takes { "priority": <int> } (clamped to at least 16) and returns { "success": true, "priority": ... }.
POST /zones/{id}
Section titled “POST /zones/{id}”Rename a custom zone (also accepts PUT / PATCH). Body: { "name": "..." } (max 100 characters).
Response — 200 OK: { "success": true, "zone": {...} }.
Errors: invalid_params (400), not_found (404), system_zone_immutable (403, the Auto ROW zone), mirror_zone_immutable (403, zones mirrored from WooCommerce shipping zones — rename those in WooCommerce).
POST /telemetry-consent
Section titled “POST /telemetry-consent”Save beta telemetry consent. Requires manage_options. Body is either { "consent": bool, "usage": bool, "errors": bool, "plugins": bool } or { "dino_telemetry_choice": "enable" | "decline" | "customize" }.
Response — 200 OK: { "success": true, "consent": { "consent": ..., "usage": ..., "errors": ..., "plugins": ... } }.
Errors: dino_invalid_consent_request (400).
Rule snapshots
Section titled “Rule snapshots”Historical copies of the rule set written on every POST /rules. Used for audit and restore. Up to 50 unpinned snapshots are kept.
GET /snapshots
Section titled “GET /snapshots”List snapshot metadata (no rule bodies), newest first, 20 most recent.
Response — 200 OK
[ { "id": 42, "snapshot_name": "Pre-Black-Friday", "comment": "...", "user_id": 1, "user_name": "Mark", "diff_summary": "Modified: 10% off Summer.", "created_at": "2026-04-18 09:00:00" }]GET /snapshots/{id}
Section titled “GET /snapshots/{id}”Fetch a single snapshot including its full rules payload.
Response — 200 OK: the snapshot row with a populated rules array and a promotion_meta map.
Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No snapshot with that id. |
| 500 | snapshot_corrupt | The stored rule data is invalid. Delete and recreate. |
POST /snapshots/{id}
Section titled “POST /snapshots/{id}”Update snapshot metadata. Body accepts snapshot_name (max 200 bytes) and/or comment (max 4000 bytes); omitted fields are left unchanged.
Response — 200 OK
{ "success": true }Errors: rest_invalid_param (400, over the byte cap), not_found (404), db_error (500).
DELETE /snapshots/{id}
Section titled “DELETE /snapshots/{id}”Delete a snapshot.
Response — 200 OK: { "success": true }.
Errors: cannot_delete (404) — the snapshot no longer exists.
GET /snapshots/{id}/restore-preview
Section titled “GET /snapshots/{id}/restore-preview”What a restore would change, without writing anything.
Response — 200 OK: { "added": 1, "overwritten": 2, "added_names": [...], "overwritten_names": [...] }.
Errors: not_found (404), snapshot_corrupt (500).
POST /snapshots/{id}/restore
Section titled “POST /snapshots/{id}/restore”Merge this snapshot’s rules into the active rule set: a snapshot rule with the same id as a live rule overwrites it, a snapshot rule new to the store is added, and live rules not in the snapshot are kept. Nothing is deleted. A new snapshot recording the restore is written.
Response — 200 OK
{ "success": true, "rules": [ /* the merged rule set */ ] }Errors: not_found (404), snapshot_unrestorable (400) — a snapshot rule no longer passes validation; the active rules are unchanged.
Example
curl -X POST "https://example.com/wp-json/dino-discounts/v1/snapshots/42/restore" \ -H "X-WP-Nonce: $NONCE" \ --cookie "$WP_COOKIE"GET /snapshots/{id}/export
Section titled “GET /snapshots/{id}/export”Download a snapshot’s full payload without restoring it.
Response — 200 OK: { "format": "dino-discounts/snapshot-export", "format_version": 1, "snapshot": { "id", "snapshot_name", "comment", "created_at", "user_name", "diff_summary" }, "rules": [...], "promotion_meta": {...} }.
Errors: not_found (404), snapshot_corrupt (500).
Cart preview
Section titled “Cart preview”Run the discount engine against a hypothetical cart with in-memory rules — no session, no persistence. Used by the admin Cart Preview.
POST /preview
Section titled “POST /preview”Request body
| Field | Type | Required | Notes |
|---|---|---|---|
cart | array | ✅ | Array of { product_id, variation_id, quantity, price, name, isCustom }. Max 500 rows. |
rules | array | ✅ | Rule set to evaluate (does not have to match the saved rules). |
global_settings | object | — | If set, replaces the store’s global settings for this evaluation. |
scope | string | — | all-active-rules (default) or this-rule-draft. With this-rule-draft, an incomplete rule returns a 400; otherwise incomplete rules are listed in skipped. |
cart_total | number | — | Ignored — recalculated server-side from cart. |
currency | string | — | Preview currency (e.g. USD). With WCPBC, the country’s zone currency can override it; the response’s effective_currency is what was used. |
country | string | — | ISO-2 country code. |
user_role | string | — | Simulate a specific user role (e.g. customer, subscriber). |
is_logged_in | bool | — | Simulate a logged-in or guest shopper. |
coupon_codes | array of string | — | Simulated applied coupon codes. |
coupon_code | string | — | Legacy single-code param. Prefer coupon_codes. |
order_count | int | — | Simulated customer order count. |
total_spent | number | — | Simulated customer total spent. |
days_since_last_order | int | — | Simulated history field. |
simulate_date | string | — | Evaluate as if on this date. |
simulate_time | string | — | HH:MM in the store timezone. |
day_of_week | string | — | Simulated weekday. |
debug | bool | — | If true, includes per-rule trace output. |
Response — 200 OK
{ "success": true, "discounts": [ { "rule_id": "3f2b8c1e-…", "type": "tiered", "name": "10% off Summer", "cart_label": "Summer 10%", "amount": 3.50, "percent": 10, "reward_type": "percent_off" /* , ... */ } ], "cart": [ /* sanitised + repriced cart */ ], "cart_total": 35.00, "cart_tax": 0, "effective_currency": "GBP", "price_format": { "prefix": "£", "suffix": "", "decimals": 2, "decimal_separator": ".", "thousand_separator": "," }, "cart_label_visible": true, "nudge": null, "coupon_recognition": { "SUMMER10": "..." }, "skipped": [], "trace": [ /* only when debug=true */ ], "global_settings_trace": { /* only when debug=true */ }}Each discounts[] entry is an engine discount result; type is the rule’s rewards.kind (tiered, bulk, pooled_match). When WP_DEBUG is defined and truthy, an additional _debug object is included.
price_format is the cart’s money format for effective_currency: the symbol where wc_price() puts it, with the store’s separators and that currency’s precision. cart_label_visible is false when the store hides the discount label on the cart page; the preview then hides its discount rows, as the cart does.
Errors: rule.schema_invalid (400, scope: "this-rule-draft" only), rest_invalid_param (400, cart or rules malformed or over the cap).
Cart scenarios
Section titled “Cart scenarios”Merchant-saveable named cart-preview contexts (items + filters), shared across admins. Stored in the dino_discounts_cart_scenarios option. On first read, the default scenarios are seeded (up to eight, built from the store’s catalogue).
GET /cart-scenarios
Section titled “GET /cart-scenarios”Response — 200 OK
{ "scenarios": [ { "id": "single-low-value", "name": "Single low-value item", "is_default": true, "items": [ { "product_id": 12, "variation_id": 0, "quantity": 1, "price": 9.99, "name": "Widget", "is_stale": false } ], "filters": { "currency": "USD", "region": "US", "customerRole": "", "isLoggedIn": false, "couponCodes": [] }, "created_at": "2026-05-12T09:00:00+00:00", "updated_at": "2026-05-12T09:00:00+00:00" } ]}Each item carries is_stale: true if the referenced product no longer exists in the catalogue.
POST /cart-scenarios
Section titled “POST /cart-scenarios”Create a new scenario. The id is server-assigned.
Request body
| Field | Type | Required |
|---|---|---|
name | string | ✅ |
items | array | — |
filters | object | — |
Response — 200 OK: { "success": true, "scenario": {...} }.
Errors: invalid_params (400).
PATCH /cart-scenarios/{id}
Section titled “PATCH /cart-scenarios/{id}”Partial update (also accepts POST / PUT). Any of name, items, filters may be omitted — omitted fields are left unchanged. Send expected_updated_at (the updated_at you last read) to get a 409 instead of overwriting another admin’s edit.
Response — 200 OK: { "success": true, "scenario": {...} }.
Errors: invalid_params (400), scenario_not_found (404), scenario_conflict (409).
DELETE /cart-scenarios/{id}
Section titled “DELETE /cart-scenarios/{id}”Delete a scenario (defaults and merchant-saved alike).
Response — 200 OK: { "success": true, "id": "..." }.
Errors: scenario_not_found (404).
POST /cart-scenarios/reset-defaults
Section titled “POST /cart-scenarios/reset-defaults”Re-seed the default scenarios. Merchant-saved scenarios are preserved.
Response — 200 OK: { "success": true, "scenarios": [...] }.
Coupons
Section titled “Coupons”Manage coupons and bulk code pools used to unlock rules.
The {coupon} path segment matches [a-zA-Z0-9_\-]+. Several responses still carry a campaign key that mirrors coupon.
GET /coupons
Section titled “GET /coupons”List coupons.
Query
| Param | Type | Default |
|---|---|---|
include_archived | bool | false |
Response — 200 OK: an array of rows { coupon, campaign, activation_type, coupon_status, total, used, pct, redemptions, expires_at, url_token, ... }. activation_type is single_code, url_token or bulk_pool; coupon_status is active or archived (mirrored as campaign_status).
POST /coupons
Section titled “POST /coupons”Create a coupon.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
coupon_id | string | ✅ | The code: 1–80 characters of A–Z, 0–9, -, _ (uppercased). |
activation | object | ✅ | { "kind": "single_code" }, { "kind": "url_token", "token": "..." } (token optional — minted when absent), or { "kind": "bulk" }. |
name | string | — | Display name. |
status | string | — | active (default) or expired. |
usage_limit | int | — | Usage limit. Omitted = unlimited for single_code / url_token; bulk codes default to 1 and are never unlimited. |
url_apply_notice | string | — | Notice shown when the coupon is applied from a link. |
bulk_quantity | int | bulk only | 1–10000 codes to generate. Codes use the coupon code as prefix. |
bulk_expires_at | string | — | Bulk only. Future datetime in the store timezone. |
Response — 200 OK — single_code / url_token
{ "success": true, "coupon": "SUMMER10", "campaign": "SUMMER10", "coupon_entity": { "coupon_id": "SUMMER10", "name": null, "status": "active", "activation": { "kind": "single_code" }, "url_apply_notice": null, "usage_limit": null, "created_at": "2026-04-18 09:00:00" } }Response — 200 OK — bulk
{ "success": true, "complete": true, "partial": false, "message": "", "requested": 1000, "coupon": "SUMMER", "campaign": "SUMMER", "generated": 1000, "attempts": 1024, "collisions": 24, "exhausted": false, "stats": { "total": 1000, "used": 0, "remaining": 1000 }, "coupon_entity": { /* as above */ }}If not all codes could be generated due to collisions, complete: false, partial: true, and a message explaining the top-up path are returned.
Errors: invalid_params (400), quantity_too_large (400), campaign_exists (400, an active coupon already has this code), coupon_archived (409, an archived coupon has this code — restore it instead), coupon_string_conflict (409, the code or token is already in use elsewhere), create_failed (500).
GET /coupons/{coupon}
Section titled “GET /coupons/{coupon}”Fetch one coupon in the coupon_entity shape above.
Errors: invalid_coupon (400), not_found (404).
GET /coupons/find
Section titled “GET /coupons/find”Search coupons by code or fragment (case-insensitive). Returns up to 50 ranked matches per page.
Query
| Param | Type | Required | Notes |
|---|---|---|---|
code | string | ✅ | Full code or substring. |
offset | int | — | Default 0. |
Response — 200 OK
{ "found": true, "total": 3, "results": [ { "coupon": "SUMMER10", "campaign": "SUMMER10", "activation_type": "single_code" } ], "result": { /* first result, back-compat */ }}Errors: invalid_code (400).
GET /coupons/suggestions
Section titled “GET /coupons/suggestions”Autocomplete rows for the Cart Preview coupon field: one row per single-code / link coupon plus a few unredeemed codes per bulk pool. Each row has code, activation_type and display_label.
GET /coupons/validate-refs
Section titled “GET /coupons/validate-refs”Check rule coupon references. Query ids=A,B,C (or ids[]=). Returns only the broken ones: { "broken": { "FOO": "missing", "BAR": "inactive" } }. An empty map means every reference can fire.
GET /coupons/generate-random
Section titled “GET /coupons/generate-random”Generate a unique random coupon code without saving it.
Query
| Param | Type | Required |
|---|---|---|
prefix | string | — (max 20 characters) |
Response — 200 OK: { "success": true, "code": "PFX-4F9A-12BD" }.
Errors: code_generation_failed (500).
GET /coupons/wc-clash
Section titled “GET /coupons/wc-clash”Check whether a WooCommerce native coupon already owns the given code.
Query
| Param | Type | Required |
|---|---|---|
code | string | ✅ (else returns clash: false) |
Response — 200 OK: { "clash": true, "code": "SUMMER10" }.
GET /coupons/orphan-check
Section titled “GET /coupons/orphan-check”Given a snapshot id, list active coupons whose linked rule would stop referencing them if that snapshot were restored.
Query
| Param | Type | Required |
|---|---|---|
snapshot_id | int | ✅ |
Response — 200 OK: { "orphaned": ["SUMMER10", "FALL20"] }.
Errors: invalid_snapshot (400), not_found (404).
GET / POST /coupons/{coupon}/bulk-codes
Section titled “GET / POST /coupons/{coupon}/bulk-codes”GET lists a bulk pool’s codes: { "coupon_id": "...", "codes": [...] }. POST adds one code: body { "code": "...", "usage_limit": 1, "expires_at": "..." } (usage_limit defaults to 1); returns { "success": true, "coupon_id": "...", "code": "..." }.
Errors: invalid_coupon (400), invalid_activation (400, not a bulk coupon), invalid_params (400), not_found (404), code_already_in_pool (409), coupon_string_conflict (409), create_failed (500).
DELETE /coupons/{coupon}/bulk-codes/{code}
Section titled “DELETE /coupons/{coupon}/bulk-codes/{code}”Remove one code from a bulk pool. Returns { "success": true, "coupon_id": "...", "code": "..." }.
GET /coupons/{coupon}/history
Section titled “GET /coupons/{coupon}/history”Generation-batch audit trail for a bulk coupon, annotated with a per-user “downloaded” flag.
Response — 200 OK
{ "batches": [ { "id": 7, "quantity": 500, "batch_no": 1, "generated_by": 3, "generated_by_label": "Mark", "generated_at": "2026-04-01 12:00:00", "downloaded": true } ]}Errors: invalid_coupon (400).
GET /coupons/{coupon}/history/{batch_id}/codes
Section titled “GET /coupons/{coupon}/history/{batch_id}/codes”Paginated list of one batch’s codes. Query page, per_page (default 50, max 200).
Response — 200 OK: { "batch_id": 7, "page": 1, "per_page": 50, "total": 500, "total_pages": 10, "codes": [...] }.
Errors: invalid_params (400), not_found (404).
POST /coupons/{coupon}/history/{batch_id}/mark-downloaded
Section titled “POST /coupons/{coupon}/history/{batch_id}/mark-downloaded”Mark a generation batch as “downloaded” for the current user (audit flag).
Response — 200 OK: { "success": true, "batch_id": 7 }.
Errors: invalid_params (400), rest_not_logged_in (401), not_found (404).
GET /coupons/{coupon}/export
Section titled “GET /coupons/{coupon}/export”Download codes as CSV.
Query
| Param | Value | Effect |
|---|---|---|
batch_id | int | Export just one generation batch. |
format | info or audit | Include batch metadata columns. audit is an alias of info. |
Response: text/csv with Content-Disposition: attachment. Not a JSON response — the handler streams bytes and exits.
Errors: invalid_coupon (400), not_found (404).
GET /coupons/{coupon}/export-txt
Section titled “GET /coupons/{coupon}/export-txt”Download all codes as plain text (one per line). Response: text/plain attachment.
Errors: invalid_coupon (400).
GET /coupons/export-table
Section titled “GET /coupons/export-table”Download a summary CSV of all coupons (one row per coupon).
Response: text/csv attachment, filename dino-campaigns.csv.
GET /coupons/export-full
Section titled “GET /coupons/export-full”Download a ZIP containing the summary CSV plus one CSV per bulk-pool coupon.
Response: application/zip attachment, filename dino-campaigns-export-<date>.zip (the summary inside is campaigns.csv).
Errors: zip_unavailable (500) if PHP ZipArchive is missing; zip_open_failed (500).
POST /coupons/{coupon}/archive
Section titled “POST /coupons/{coupon}/archive”Archive a coupon (it stops applying; codes and redemption history are kept).
Response — 200 OK: { "success": true, "coupon": "SUMMER10", "campaign": "SUMMER10" }.
Errors: invalid_coupon (400).
POST /coupons/{coupon}/restore
Section titled “POST /coupons/{coupon}/restore”Restore an archived coupon.
Response — 200 OK: { "success": true, "coupon": "SUMMER10", "campaign": "SUMMER10" }.
Errors: invalid_coupon (400), not_found (404), not_archived (400).
POST /coupons/{coupon}/generate
Section titled “POST /coupons/{coupon}/generate”Generate more codes for an existing active bulk-pool coupon. Inherits the coupon’s usage_limit.
Request body
| Field | Type | Required |
|---|---|---|
quantity | int | ✅ (1 ≤ q ≤ 10000) |
Response — 200 OK: same shape as bulk create (complete, partial, generated, collisions, stats, …).
Errors: rest_invalid_param (400, quantity out of range), invalid_params (400), not_found (404, no active coupon).
DELETE /coupons/{coupon}/delete
Section titled “DELETE /coupons/{coupon}/delete”Permanently delete a coupon (active or archived) with all its codes and generation-log rows.
Response — 200 OK: { "success": true, "coupon": "SUMMER10", "campaign": "SUMMER10" }.
Errors: invalid_coupon (400), not_found (404).
Analytics
Section titled “Analytics”The read routes accept from / to (YYYY-MM-DD, store timezone) and default to the last 30 days.
GET /analytics/discounts
Section titled “GET /analytics/discounts”Per-rule application counts, discount amounts, gross revenue, time series, and order-value distributions, plus promotion rollups.
Query
| Param | Type | Default | Constraint |
|---|---|---|---|
from | date | now − 30d | YYYY-MM-DD |
to | date | now | YYYY-MM-DD |
per_page | int | 25 | 1–100 |
page | int | 1 | ≥1 |
Response — 200 OK
{ "rules": [ { "rule_id": "3f2b8c1e-5d4a-4b6f-9a1e-7c2d0e8f4a11", "currency": "GBP", "rule_name": "10% off Summer", "rule_type": "tiered", "is_active": true, "promotion_id": null, "application_count": 120, "order_count": 95, "total_amount": 412.50, "total_gross": 3720.00, "whole_order_gross": 4100.00, "aov_with_rule": 39.16, "new_customer_pct": 22.1, "new_customers": 21, "gifts_given": null, "time_series": [ { "date": "2026-04-01", "application_count": 4, "order_count": 3, "total_amount": 15.00, "total_gross": 120.00, "aov": 40.00 } ], "distribution": [ { "bucket": 30, "label": "30-39", "count": 12 } ] } ], "promotions": [ /* per-promotion rollups */ ], "ungrouped": { /* rollup of rules outside any promotion */ }, "totals": { "total_orders_with_discounts": 95, "total_discount_amount": 412.50, "total_discount_by_currency": [ { "currency": "GBP", "amount": 412.50 } ], "total_store_orders": 540 }, "pagination": { "page": 1, "per_page": 25, "total_rules": 7 }, "date_range": { "from": "2026-03-19", "to": "2026-04-18", "timezone": "Europe/London" }}Rows are per rule and currency, busiest first: ordered by order_count, then total_amount. gifts_given is the number of orders a free-gift rule’s gift went out on, and null for a rule that gives no gift. total_discount_amount is null when discounts were given in more than one currency — use total_discount_by_currency.
Errors: rest_invalid_param (400) on malformed from / to.
GET /analytics/coupons
Section titled “GET /analytics/coupons”Per-coupon snapshot (codes issued, redeemed, remaining, utilisation%) and a per-coupon daily time series. gifts_given counts the orders in the window that received the gift of a coupon whose discount gives one, and is null otherwise.
Query: from, to (same semantics as above).
Response — 200 OK
{ "campaigns": [ { "campaign": "SUMMER10", "rule_id": "3f2b8c1e-5d4a-4b6f-9a1e-7c2d0e8f4a11", "rule_name": "10% off Summer", "activation_type": "single_code", "codes_issued": 1, "codes_redeemed": 47, "codes_remaining": 0, "utilisation_pct": 4700.0, "last_used_at": "2026-04-15 14:22:01", "expires_at": null, "gifts_given": null } ], "time_series": { "SUMMER10": [ { "date": "2026-04-01", "redemptions": 4, "discount_total": 25.00, "discount_by_currency": [ { "currency": "GBP", "amount": 25.00 } ] } ] }, "totals": { "total_campaigns": 1, "total_codes_issued": 1, "total_codes_redeemed": 47, "total_discount_given": 235.00, "total_discount_by_currency": [ { "currency": "GBP", "amount": 235.00 } ] }, "date_range": { "from": "2026-03-19", "to": "2026-04-18" }, "timezone": "Europe/London"}GET /analytics/timing
Section titled “GET /analytics/timing”Day-of-week and redemption-velocity insights: { rules, store, logged_split, velocity, dow_windowed, date_range }. Only velocity follows from / to. The day-of-week figures are all-time, which dow_windowed: false signals.
GET /analytics/export
Section titled “GET /analytics/export”Stream raw event rows as CSV for the same date window.
Response: text/csv attachment dino_analytics_<from>_to_<to>.csv with columns Order ID,Rule ID,Date,Currency,Discount Amount,Gross Revenue,Is New Customer.
Example
# -J (use server filename) requires -O. Combined short form -OJ is portable;# -O -J or -J -O also work. Some shells glob differently — the combined form# is safest in zsh.curl -OJ "https://example.com/wp-json/dino-discounts/v1/analytics/export?from=2026-04-01&to=2026-04-18" \ -H "X-WP-Nonce: $NONCE" \ --cookie "$WP_COOKIE"DELETE /analytics/data
Section titled “DELETE /analytics/data”Permanently delete every recorded analytics event.
Response — 200 OK: { "success": true, "deleted": 1234 }.
Errors: analytics_delete_failed (500).
Product & taxonomy search
Section titled “Product & taxonomy search”Search helpers powering the admin rule-builder pickers.
GET /products/search
Section titled “GET /products/search”Query
| Param | Type | Default | Notes |
|---|---|---|---|
search | string | "" | Free-text title / SKU. Empty returns first page. |
page | int | 1 | Capped at 1000. |
per_page | int | 20 | Capped at 100. |
include | string | "" | Comma-separated product IDs to hydrate (bypasses search/page). |
category | string | "" | Comma-separated category slugs. |
tag | string | "" | Comma-separated tag slugs. |
min_price | number | — | |
max_price | number | — | |
currency | string | "" | With country, adds a WCPBC zone price to each row when one exists. |
country | string | "" |
Response — 200 OK
{ "items": [ { "id": 42, "name": "Widget - Blue", "price": "9.99", "sku": "WIDGET-BLUE", "images": [{"src": "..."}], "type": "variation", "stock_status": "instock", "parent_id": 41 } ], "total": 120, "totalPages": 6}Variation names are rendered Parent Product - Variation Label. Variation rows carry parent_id; rows priced for a WCPBC zone carry zone_price and zone_currency.
POST /products/resolve-skus
Section titled “POST /products/resolve-skus”Resolve pasted SKUs. Body { "skus": ["A-1", "B-2"] } (up to 500). Each result has a status of resolved, ambiguous (several products share the SKU) or not_found (with a suggestion where one exists).
Response — 200 OK: { "results": [...], "truncated": false }.
GET /taxonomy/search
Section titled “GET /taxonomy/search”Query
| Param | Type | Required | Notes |
|---|---|---|---|
taxonomy | string | ✅ | product_cat, product_tag, or any pa_* global attribute taxonomy. |
search | string | — | Free-text name/slug search (up to 30 results). |
include | string | — | Comma-separated term IDs (product_cat, product_tag) or slugs (pa_*) to resolve by identity. Max 200. |
Response — 200 OK
[ { "id": 10, "slug": "accessories", "name": "Accessories" } ]Errors: rest_invalid_param (400) on an unsupported taxonomy (the detail is invalid_taxonomy); taxonomy_include_too_many (400).
Diagnostics
Section titled “Diagnostics”GET /performance/log
Section titled “GET /performance/log”Read accumulated PerformanceMonitor entries (admin REST spans, engine evaluations).
Response — 200 OK: { "entries": [...], "count": 42 } — or { "entries": [] } if monitoring isn’t available.
DELETE /performance/log
Section titled “DELETE /performance/log”Clear the log.
Response — 200 OK: { "success": true }.
GET /cache-compat
Section titled “GET /cache-compat”Check active page-cache plugins for missing compatibility settings.
Response — 200 OK: { "compatible": true, "incompatibilities": [ { "plugin": "...", "missing": [...] } ] }.
GET /bug-report/debug-payload
Section titled “GET /bug-report/debug-payload”The environment summary attached to bug reports (plugin / WP / WC / PHP versions, theme, rules summary, checkout and cart type, HPOS, base currency and country, …). Add ?detailed=1 to include the active plugins list.
POST /bug-report/submit
Section titled “POST /bug-report/submit”Submit a bug report or feedback. Body: title, body, type (bug, idea, general, nps), optional email with email_consent: true.
Response — 200 OK: { "success": true, "delivered": "worker" | "email" | "dropped", ... }.
Errors: rest_forbidden (401/403), rest_too_many_requests (429), rest_rate_limit_unavailable (503), bug_report_invalid (400), bug_report_consent_required (400), bug_report_too_large (413), 502 when delivery fails.
POST /telemetry/error
Section titled “POST /telemetry/error”Registered only when a telemetry endpoint is configured, which every release zip is. Accepts an admin JS error (message, source, lineno, kind) and forwards it only when the merchant has opted in to error telemetry. Always returns { "accepted": bool }.
Compatibility Center
Section titled “Compatibility Center”Backs the Dev Tools Compatibility Center.
GET /compat
Section titled “GET /compat”List the compatibility entries with their detection status and state: { "entries": [ { "entry", "status", "state", "verdict", "probe", "verification", "installed_version", "applying", "toggleable", "needs_acknowledgement", ... } ], "empty": false }.
POST /compat/{id}
Section titled “POST /compat/{id}”Turn a fix on or off. Body: { "enabled": true | false, "acknowledge_out_of_range": bool }. Returns the updated entry row.
Errors: dino_compat_unknown_entry (404), dino_compat_not_toggleable (400), dino_compat_invalid_payload (400), dino_compat_not_applicable (409), dino_compat_stood_down (409), dino_compat_needs_acknowledgement (409).
POST /compat/{id}/probe
Section titled “POST /compat/{id}/probe”Public. Called by the storefront probe asset to record one measurement. Requires the entry’s probe token in the X-Dino-Compat-Probe header and an outcome of rule-present, rule-absent or subject-absent. It can never turn an entry on or off. Returns { "stored": true }.
Errors: dino_compat_probe_refused (400), dino_compat_probe_throttled (429).
Demo store
Section titled “Demo store”POST /demo-store/seed
Section titled “POST /demo-store/seed”Load the demo store’s products, discounts and coupon. Requires manage_options.
Response — 200 OK: { "success": true, "products": 12, "rule_count": 8, "promotions": 2, "coupon": "..." }.
Errors: demo_store_unavailable (409) — WooCommerce is not active.
Onboarding
Section titled “Onboarding”Per-admin-user first-run state backing the welcome panel, the sample-rule activation path, the first-success callout and the “skip setup” dismissal. The three HTTP methods share one route in OnboardingController::register_routes().
Storage: per-user WordPress user-meta keyed by dino_discounts_onboarding_state (not the options table) — each admin user has an independent first-run experience. See OnboardingController::USER_META_KEY.
State shape (all fields optional; missing keys fall back to defaults):
| Key | Type | Notes |
|---|---|---|
welcome_skipped | bool | The admin clicked “skip setup”. |
welcome_completed | bool | Welcome panel finished (either by publishing a rule or explicit done). |
first_rule_callout_dismissed | bool | First-publish success callout was closed. |
first_publish_ts | int | Unix seconds — set once when the user first publishes. |
activated_ts | int | Unix seconds — set on first admin-page load. Preserved across a DELETE. |
Unknown keys in a POST body are dropped silently.
GET /onboarding
Section titled “GET /onboarding”Return the current user’s onboarding state, merged over defaults.
Response — 200 OK
{ "welcome_skipped": false, "welcome_completed": true, "first_rule_callout_dismissed": false, "first_publish_ts": 1713960000, "activated_ts": 1713800000}POST /onboarding
Section titled “POST /onboarding”Persist a partial update. The incoming state object is merged over the stored blob — callers can update a single flag without losing the rest.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
state | object | ✅ | Partial state. Keys outside the table above are ignored. |
Example
curl -X POST \ -H "Content-Type: application/json" \ -H "X-WP-Nonce: <nonce>" \ --cookie "wordpress_logged_in_*=..." \ -d '{ "state": { "welcome_completed": true } }' \ "https://example.com/wp-json/dino-discounts/v1/onboarding"Response — 200 OK: { "success": true, "state": { ... merged state ... } }.
Errors: rest_missing_callback_param / rest_invalid_param (400) if state is missing or not an object; onboarding_persist_failed (500).
DELETE /onboarding
Section titled “DELETE /onboarding”Reset the current user’s state to defaults. activated_ts is preserved; everything else goes back to defaults.
Response — 200 OK: { "success": true, "state": { ... defaults with preserved activated_ts ... } }.
Store API extension
Section titled “Store API extension”WooCommerce Blocks cart/checkout and headless clients that consume the Store API get Dino Discounts data through the ExtendSchema mechanism (includes/Cart/StoreApiExtension.php). The plugin does not register any custom Store API routes — it attaches data to the existing cart and cart-item responses and registers one cart/extensions update callback.
Endpoints touched
Section titled “Endpoints touched”Every Store API response that returns the cart, e.g. GET /wp-json/wc/store/v1/cart and the cart mutation routes.
Auth: standard Store API (cart token / Nonce header). No manage_woocommerce requirement — this is the shopper-facing cart.
Added keys
Section titled “Added keys”The cart response gains extensions["dino-discounts"]:
{ "extensions": { "dino-discounts": { "discounts": [ { "rule_id": "3f2b8c1e-5d4a-4b6f-9a1e-7c2d0e8f4a11", "rule_name": "Summer 10%", "kind": "amount", "discount_type": "tiered", "percent": 10, "amount": "350", "amount_display": "350", "is_virtual": true, "show_mini": true, "held_zero": false, "allow_removal": true, "is_coupon_activated": false } ], "declined_offers": [], "removal": { "restore_row": { "cart": true, "checkout": true } }, "chip_config": { "dino_dd_3f2b8c1e-5d4a-4b6f-9a1e-7c2d0e8f4a11": { "label": "Summer 10%", "visible_cart": true, "visible_checkout": true } }, "per_item": {}, "gift_product_ids": [], "gift_item_keys": [], "nudges_mini": {}, "nudges_cart": {}, "nudges_checkout": {}, "cart_notice": [], "url_apply_notice": [], "owned_apply_code": "", "reject_proxy_codes": [], "silenced_removals": [] } }}Field reference
Section titled “Field reference”discounts[] — one entry per applied Dino virtual coupon on the cart.
| Field | Type | Notes |
|---|---|---|
rule_id | string | The engine rule id. |
rule_name | string | The resolved cart label (falls back to “Discount”). |
kind | string | amount, free_shipping or gift: how the line’s amount cell reads (“Free” unless amount). |
discount_type | string | Mirrors rewards.kind — tiered, bulk, pooled_match. |
percent | number | 0 for fixed-amount discounts. |
amount | string | Minor units relative to the cart response’s totals.currency_minor_unit (the Store API convention — divide by 10^currency_minor_unit). |
amount_display | string | The same amount in the cart’s display tax basis, in minor units. |
is_virtual | boolean | Always true for Dino auto-applied coupons. |
show_mini | boolean | Whether the label shows in the mini-cart. |
held_zero | boolean | The coupon is applied but currently discounts nothing. |
allow_removal | boolean | Whether the shopper may remove this discount. |
is_coupon_activated | boolean | Fired by a typed coupon code rather than auto-applied. |
chip_config — per-applied-coupon display config keyed by the internal coupon code, CartCoupons::COUPON_PREFIX + rule id (dino_dd_<rule_id>). Each entry is { "label": string, "visible_cart": bool, "visible_checkout": bool }: the resolved cart label plus whether it shows on the block cart and checkout. Visibility comes from the store-wide Message Settings (label_visibility.cart / label_visibility.checkout), not from the rule. Only applied coupons are keyed. This is the only source of block coupon-chip labels: render label in place of the raw dino_dd_ code and honour the visibility flags. See StoreApiExtension::build_chip_config().
Other cart-level keys:
| Key | Notes |
|---|---|
declined_offers | Discounts the shopper removed this session that still qualify: { rule_id, label, is_gift }. |
removal.restore_row | Whether the restore row shows on cart / checkout (from Message Settings). |
per_item | Per-cart-item-key discount amounts in minor units. |
gift_product_ids / gift_item_keys | Free-gift lines. |
nudges_mini / nudges_cart / nudges_checkout | Spend-more nudge HTML keyed by the store-wide position for each surface; empty when no rule nudges or the position is hidden. |
cart_notice | Resolved standalone cart-notice strings, one per applied rule that shows one. |
url_apply_notice | Discount-link rejection messages, consumed on read. |
owned_apply_code | Set on an apply-coupon response when the code is a Dino coupon, so the block client can skip WooCommerce’s duplicate snackbar. |
reject_proxy_codes | Typed codes held as inert reject proxies; render no chip for them. |
silenced_removals | Coupon codes whose removal shows no notice. |
Cart items gain extensions["dino-discounts"] with is_free_gift, gift_label_hidden_surfaces, gift_removal_locked and gift_rule_id.
Re-applying a removed discount
Section titled “Re-applying a removed discount”POST /wp-json/wc/store/v1/cart/extensions with { "namespace": "dino-discounts", "data": { "action": "reapply_discount", "rule_id": "<rule id>" } } restores a discount the shopper removed. Unknown actions are ignored. The plugin also lets the standard cart/remove-coupon route remove a dino_dd_ coupon.
Graceful no-op
Section titled “Graceful no-op”If the Store API is not available (Automattic\WooCommerce\StoreApi\StoreApi class missing), the extension silently no-ops — the cart response is unmodified.
Error shape
Section titled “Error shape”All errors follow the standard WordPress REST shape — a WP_Error serialized as JSON:
{ "code": "too_many_rules", "message": "This store has reached the 200-rule limit. Remove or archive an unused rule before saving.", "data": { "status": 400 }}The HTTP status mirrors data.status. Error codes are referenced per endpoint throughout this doc.