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 32 admin REST routes under /wp-json/dino-discounts/v1/ — counted as register_rest_route() calls in includes/API/. One of those calls (/onboarding) bundles three HTTP methods (GET / POST / DELETE) into a single registration, so the HTTP-method count is slightly higher. Plus a WooCommerce Store API extension that decorates /wp-json/wc/store/v1/cart with active-discount metadata. This page is the authoritative reference for integrators — every route below matches a register_rest_route() call in includes/API/.
Rex rex. Enjoy the docs.
- 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
- Rules —
/rules,/rules/diff-summary,/rules/draft - Global settings —
/global_settings - Rule snapshots —
/snapshots - Cart preview —
/preview - Cart scenarios —
/cart-scenarios - Coupon campaigns —
/coupon-campaigns - Analytics —
/analytics/* - Product & taxonomy search —
/products/search,/taxonomy/search - Diagnostics —
/performance/log - Onboarding —
/onboarding - Store API extension —
wc/store/v1/cartextensions.dino-discounts - Error shape
Authentication
Section titled “Authentication”Every route registers AbstractController::check_permission() as its permission_callback. That callback requires the current user to have the manage_woocommerce capability. Unauthenticated or under-privileged requests get the standard WordPress rest_forbidden / rest_cookie_invalid_nonce response (HTTP 401 / 403).
There are no public routes — every route is admin-only.
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.dinoDiscountsAdminData.nonce (includes/Admin.php:311). |
| 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.dinoDiscountsAdminData.nonce.
Common auth failures
Section titled “Common auth failures”| Status | Error code | Meaning |
|---|---|---|
| 401 | rest_not_logged_in | No cookie / application password. |
| 403 | rest_forbidden | Logged in but missing manage_woocommerce. |
| 403 | rest_cookie_invalid_nonce | Nonce header missing, 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.
Pagination
Section titled “Pagination”Only /analytics/discounts, /analytics/export, and /products/search paginate. They all use the same pattern:
page— 1-indexed, default1per_page— default varies (25 for analytics, 20 for products), capped at 100- Response includes a
paginationobject (analytics) or top-leveltotal/totalPages(products).
Other list routes (snapshots, scenarios, campaigns, etc.) return the full collection in one response. These lists are bounded by the domain (200 rules max, small snapshot counts in practice).
Rate limiting
Section titled “Rate limiting”None is enforced by the plugin. Bulk coupon generation is capped at 10,000 codes per request; rule saves are capped at 200 rules total.
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.
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": [ { "id": "rule-abc", "name": "10% off Summer", "type": "tiered", "is_active": true, "priority": 1, "targeting_tree": { "children": [...] }, "tiers": [...] } ], "draft": { "rules": [...], "global_settings": {...} }}draft is omitted if no draft is saved.
POST /rules
Section titled “POST /rules”Overwrite the active rule set. The engine cache is invalidated and an audit snapshot is written; if the snapshot write fails, the save is rolled back.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
rules | array | ✅ | List of rule objects. Order is the source of truth for evaluation priority. 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 */ ] }Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_data | rules is not an array. |
| 400 | too_many_rules | More than 200 rules supplied. |
| 400 | unknown_rule_field | RuleSanitizer rejected an unknown or malformed field. |
| 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" \ -d '{"rules":[{"id":"rule-abc","name":"10% off Summer","type":"tiered","is_active":true,"tiers":[{"min":2,"percent":10}]}]}'Fires: dino_discounts_rules_saved, 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. Useful for diff previews in the admin UI.
Request body: same rules array as POST /rules.
Response — 200 OK
{ "diff_summary": { "added": [...], "removed": [...], "changed": [...] }}Errors: invalid_data, too_many_rules, unknown_rule_field (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 — the draft is a display-layer convenience.
Request body
| Field | Type | Required |
|---|---|---|
rules | array | ✅ |
global_settings | object | ✅ |
Response — 200 OK
{ "success": true }Errors: too_many_rules (400), unknown_rule_field (400).
DELETE /rules/draft
Section titled “DELETE /rules/draft”Discard the saved draft.
Response — 200 OK
{ "success": true }Global settings
Section titled “Global settings”Store-wide discount configuration (fallback stacking mode, display strings, custom zones, performance flags).
GET /global_settings
Section titled “GET /global_settings”Response — 200 OK
Returns the full dino_discounts_global_settings option. Shape is defined by GlobalSettingsDefaults::DEFAULTS.
POST /global_settings
Section titled “POST /global_settings”Request body
| Field | Type | Required | Notes |
|---|---|---|---|
settings | object | ✅ | Full or partial settings object. Omitting custom_zones preserves the existing zones (a missing key is not treated as “clear the zones”). |
Response — 200 OK
{ "success": true, "settings": { /* sanitised settings */ } }Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_data | settings is missing or not an object. |
Fires: dino_discounts_settings_saved, dino_discounts_cache_flush.
Note: this route is registered under the namespace path
/global_settings(underscore) — the only underscore in the route inventory. Keep it exact.
Rule snapshots
Section titled “Rule snapshots”Historical copies of the rule set written on every POST /rules. Used for audit and restore.
GET /snapshots
Section titled “GET /snapshots”List snapshot metadata (no rule bodies).
Response — 200 OK
[ { "id": 42, "snapshot_name": "Pre-Black-Friday", "comment": "...", "created_at": "2026-04-18T09:00:00Z", "rule_count": 14, "pinned": true }]GET /snapshots/{id}
Section titled “GET /snapshots/{id}”Fetch a single snapshot including its full rules payload.
Response — 200 OK: the snapshot object with a populated rules array.
Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No snapshot with that id. |
| 500 | snapshot_corrupt | The stored rules_json blob is invalid. Delete and recreate. |
POST /snapshots/{id}
Section titled “POST /snapshots/{id}”Update snapshot metadata (rename, edit comment, pin). Body accepts snapshot_name and/or comment; omitted fields are left unchanged.
Response — 200 OK
{ "success": true }Errors: 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.
POST /snapshots/{id}/restore
Section titled “POST /snapshots/{id}/restore”Replace the active rule set with the contents of this snapshot. Invalidates the engine cache.
Response — 200 OK
{ "success": true, "rules": [ /* restored rules */ ] }Errors: not_found (404).
Example
curl -X POST "https://example.com/wp-json/dino-discounts/v1/snapshots/42/restore" \ -H "X-WP-Nonce: $NONCE" \ --cookie "$WP_COOKIE"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 live previewer.
POST /preview
Section titled “POST /preview”Request body
| Field | Type | Required | Notes |
|---|---|---|---|
cart | array | ✅ | Array of { product_id, variation_id, quantity, price, name, isCustom }. |
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. |
cart_total | number | — | Ignored — recalculated server-side from cart. |
currency | string | — | Preview currency (e.g. USD). If the store has WCPBC, the zone price resolver runs against this. |
country | string | — | ISO-2 country code. |
user_role | string | — | Simulate a specific user role (e.g. customer, subscriber). |
coupon_codes | array of string | — | Simulated applied coupon codes. |
coupon_code | string | — | Legacy single-code param, kept for backwards compat. 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. |
days_since_registration | int | — | Simulated history field. |
debug | bool | — | If true, includes per-rule trace output. |
Response — 200 OK
{ "success": true, "discounts": [ { "rule_id": "rule-abc", "label": "10% off Summer", "amount": 3.50, "percent": 10, "discount_type": "tiered" } ], "cart": [ /* sanitised + repriced cart */ ], "cart_total": 35.00, "trace": [ /* only when debug=true */ ]}When WP_DEBUG is defined and truthy, an additional _debug object is included.
Errors: unknown_rule_field (400).
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, six defaults are seeded.
GET /cart-scenarios
Section titled “GET /cart-scenarios”Response — 200 OK
{ "scenarios": [ { "id": "default-small-cart", "name": "Small cart", "is_default": true, "items": [ { "product_id": 12, "variation_id": 0, "quantity": 1, "price": 9.99, "name": "Widget", "is_stale": false } ], "filters": { "currency": "USD", "isLoggedIn": true } } ]}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.
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. Any of name, items, filters may be omitted — omitted fields are left unchanged.
Response — 200 OK: { "success": true, "scenario": {...} }.
Errors: invalid_params (400), not_found (404).
DELETE /cart-scenarios/{id}
Section titled “DELETE /cart-scenarios/{id}”Delete a scenario (defaults and merchant-saved alike).
Response — 200 OK: { "success": true, "id": "..." }.
POST /cart-scenarios/reset-defaults
Section titled “POST /cart-scenarios/reset-defaults”Re-seed the six default scenarios. Non-destructive: merchant-saved scenarios are preserved.
Response — 200 OK: { "success": true, "scenarios": [...] }.
Coupon campaigns
Section titled “Coupon campaigns”Manage code pools and single codes used to unlock rules.
Regex-guarded invalid_campaign errors
Section titled “Regex-guarded invalid_campaign errors”Every per-campaign route below — /history, /export, /export-txt, /archive, /delete — uses the same route regex [a-zA-Z0-9_\-]+ for the {campaign} segment. The regex requires at least one character, so the empty-string branch of each handler’s “is the campaign empty?” check is guarded away at the router — invalid_campaign (400) is unreachable via the registered route.
Handlers still contain the empty( $campaign ) check as defence-in-depth, and the error is listed per route for completeness. Integrators should not expect to see invalid_campaign (400) in practice on these routes. A malformed or missing {campaign} segment produces a 404 from WordPress’s REST router before the handler runs.
(/generate and /history/{batch_id}/mark-downloaded use the same regex guard but return the broader invalid_params error code, which remains reachable via the other parameters those handlers validate — it is not covered by this note.)
GET /coupon-campaigns
Section titled “GET /coupon-campaigns”List campaigns.
Query
| Param | Type | Default |
|---|---|---|
include_archived | bool | false |
Response — 200 OK: [ { campaign, activation_type, usage_limit, total, used, expires_at, ... } ].
POST /coupon-campaigns
Section titled “POST /coupon-campaigns”Create a campaign. The body differs by activation type.
Common fields
| Field | Type | Required | Notes |
|---|---|---|---|
campaign | string | ✅ (unless generate_random=true) | Campaign slug (A–Z, 0–9, _, -). |
activation_type | string | — | single_code (default), url_token, or bulk_pool. |
generate_random | bool | — | Server-generates a unique code. Only valid for single_code. |
usage_limit | int | — | Per-code usage limit. Default 0 (unlimited). |
expires_at | string | — | Datetime in the store timezone (parsed via DateTimeHelper::parse_store_datetime). |
bulk_pool extras
| Field | Type | Required | Notes |
|---|---|---|---|
quantity | int | ✅ | 1 ≤ quantity ≤ 10000. |
The campaign code is always used as the prefix for generated bulk codes.
Response — 200 OK — single_code / url_token
{ "success": true, "campaign": "SUMMER10", "generated_random": false }Response — 200 OK — bulk_pool
{ "success": true, "complete": true, "partial": false, "message": "", "requested": 1000, "campaign": "SUMMER", "generated": 1000, "attempts": 1024, "collisions": 24, "exhausted": false, "stats": { "total": 1000, "used": 0, "remaining": 1000 }}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), campaign_exists (400), quantity_too_large (400), code_generation_failed (500), create_failed (500).
GET /coupon-campaigns/find
Section titled “GET /coupon-campaigns/find”Search campaigns by code or fragment (case-insensitive, alphanumeric + -_). 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": [ { "campaign": "SUMMER10", "activation_type": "single_code", ... } ], "result": { /* first result, back-compat */ }}Errors: invalid_code (400).
GET /coupon-campaigns/generate-random
Section titled “GET /coupon-campaigns/generate-random”Generate a unique random campaign code without saving it.
Query
| Param | Type | Required |
|---|---|---|
prefix | string | — |
Response — 200 OK: { "success": true, "code": "PFX-4F9A-12BD" }.
Errors: code_generation_failed (500).
GET /coupon-campaigns/wc-clash
Section titled “GET /coupon-campaigns/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 /coupon-campaigns/orphan-check
Section titled “GET /coupon-campaigns/orphan-check”Given a snapshot id, list active campaigns that would lose their linked rule 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 /coupon-campaigns/{campaign}/history
Section titled “GET /coupon-campaigns/{campaign}/history”Generation-batch audit trail for a bulk campaign, annotated with per-user “downloaded” flags.
Response — 200 OK
{ "batches": [ { "batch_id": 7, "generated_at": "2026-04-01T12:00:00Z", "user_id": 3, "quantity": 500, "downloaded": true } ]}Errors: invalid_campaign (400) — unreachable via the registered route.
POST /coupon-campaigns/{campaign}/history/{batch_id}/mark-downloaded
Section titled “POST /coupon-campaigns/{campaign}/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 /coupon-campaigns/{campaign}/export
Section titled “GET /coupon-campaigns/{campaign}/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 (batch_no, times_used, generated_date, generated_by). audit is kept as an alias of info. |
Response: text/csv with Content-Disposition: attachment. Not a JSON response — the handler streams bytes and exits.
Errors: invalid_campaign (400, unreachable via the registered route), not_found (404).
GET /coupon-campaigns/{campaign}/export-txt
Section titled “GET /coupon-campaigns/{campaign}/export-txt”Download all codes as plain text (one per line). Response: text/plain attachment.
Errors: invalid_campaign (400) — unreachable via the registered route.
GET /coupon-campaigns/export-table
Section titled “GET /coupon-campaigns/export-table”Download a campaigns-summary CSV for all active campaigns (one row per campaign).
Response: text/csv attachment, filename dino-campaigns.csv.
GET /coupon-campaigns/export-full
Section titled “GET /coupon-campaigns/export-full”Download a ZIP containing the summary CSV plus one CSV per bulk-pool campaign.
Response: application/zip attachment, filename dino-campaigns-export-<date>.zip.
Errors: zip_unavailable (500) if PHP ZipArchive is missing; zip_open_failed (500).
POST /coupon-campaigns/{campaign}/archive
Section titled “POST /coupon-campaigns/{campaign}/archive”Soft-delete (archive) a campaign.
Response — 200 OK: { "success": true, "campaign": "SUMMER10" }.
Errors: invalid_campaign (400) — unreachable via the registered route.
POST /coupon-campaigns/{campaign}/generate
Section titled “POST /coupon-campaigns/{campaign}/generate”Generate more codes for an existing bulk-pool campaign. Inherits the existing usage_limit.
Request body
| Field | Type | Required |
|---|---|---|
quantity | int | ✅ (1 ≤ q ≤ 10000) |
Response — 200 OK: same shape as bulk-pool create (complete, partial, generated, collisions, stats, …).
Errors: invalid_params (400), quantity_too_large (400), not_found (404).
DELETE /coupon-campaigns/{campaign}/delete
Section titled “DELETE /coupon-campaigns/{campaign}/delete”Permanently delete an archived campaign and all associated codes + generation-log rows. The campaign must already be archived — this is enforced server-side.
Response — 200 OK: { "success": true, "campaign": "SUMMER10" }.
Errors: invalid_campaign (400, unreachable via the registered route), not_archived (400).
Analytics
Section titled “Analytics”All three analytics 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, redemption amounts, gross revenue, time series, and per-bucket order-value distributions.
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": "rule-abc", "rule_name": "10% off Summer", "rule_type": "tiered", "is_active": true, "application_count": 120, "order_count": 95, "total_amount": 412.50, "total_gross": 3720.00, "aov_with_rule": 39.16, "new_customer_pct": 22.1, "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 } ] } ], "totals": { "total_orders_with_discounts": 95, "total_discount_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" }}Errors: rest_invalid_param (400) on malformed from / to.
GET /analytics/coupons
Section titled “GET /analytics/coupons”Per-campaign snapshot (codes issued, redeemed, remaining, utilisation%) and a per-rule daily time series filtered to rules with an active campaign.
Query: from, to (same semantics as above).
Response — 200 OK
{ "campaigns": [ { "campaign": "SUMMER10", "rule_id": "rule-abc", "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 } ], "time_series": { "SUMMER10": [ { "date": "2026-04-01", "redemptions": 4, "discount_total": 25.00 } ] }, "totals": { "total_campaigns": 1, "total_codes_issued": 1, "total_codes_redeemed": 47, "total_discount_given": 235.00 }, "date_range": { "from": "2026-03-19", "to": "2026-04-18" }, "timezone": "Europe/London"}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,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"Product & taxonomy search
Section titled “Product & taxonomy search”Search helpers powering the admin rule-builder pickers. Both are GET and paginate their own way.
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 | |
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 | — |
Response — 200 OK
{ "items": [ { "id": 42, "name": "Widget — Blue", "price": "9.99", "sku": "WIDGET-BLUE", "images": [{"src": "..."}], "type": "variation", "stock_status": "instock" } ], "total": 120, "totalPages": 6}Parent-variation names are rendered Parent Product - Variation Label.
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. Anything else returns invalid_taxonomy. |
search | string | — | Free-text name/slug search (up to 30 results). |
include | string | — | Comma-separated IDs (product_cat) or slugs (product_tag, pa_*) to resolve by identity. |
Response — 200 OK
[ { "id": 10, "slug": "accessories", "name": "Accessories" } ]Errors: invalid_taxonomy on unsupported taxonomy. The controller builds this WP_Error without an explicit status argument, so WordPress defaults the HTTP response to 500 — treat a 500 on this route as a 400-class validation error.
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 compiled in.
DELETE /performance/log
Section titled “DELETE /performance/log”Clear the log.
Response — 200 OK: { "success": true }.
Onboarding
Section titled “Onboarding”Per-admin-user first-run state backing the UX-ONBOARDING welcome panel, the sample-rule activation path, the first-success callout and the “skip setup” dismissal. The three HTTP verbs live under a single register_rest_route() call in OnboardingController::register_routes() (includes/API/OnboardingController.php:149).
Auth: the shared manage_woocommerce capability (via AbstractController::check_permission()) — same as every other admin route on this page.
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 | Shopper explicitly 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 (used for ms_since_activation analytics). 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 so new keys added in later releases appear with sensible values.
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: invalid_data (HTTP 400) if state is missing or not an object.
DELETE /onboarding
Section titled “DELETE /onboarding”Reset the current user’s state to defaults. activated_ts is preserved (so analytics stays meaningful across a reset); 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 themes that consume /wp-json/wc/store/v1/cart get richer Dino-Discounts metadata via the Store API ExtendSchema mechanism. The plugin does not register any custom Store API endpoints — it only attaches data to the existing cart response.
Endpoint touched
Section titled “Endpoint touched”GET /wp-json/wc/store/v1/cart — WooCommerce Blocks standard endpoint.
Auth: standard Store API (cart token / Nonce via wc-store-api-nonce). No manage_woocommerce requirement — this is the shopper-facing cart.
Added keys
Section titled “Added keys”The extension adds extensions["dino-discounts"] to the cart response:
{ "extensions": { "dino-discounts": { "discounts": [ { "rule_id": "rule-abc", "rule_name": "10% off Summer", "discount_type": "tiered", "percent": 10, "amount": "350", "is_virtual": true } ], "chip_config": { "dino_dd_rule-abc": { "mode": "show_name", "label": "10% off Summer" } } } }}Field reference
Section titled “Field reference”discounts[] — one entry per active Dino virtual coupon on the cart.
| Field | Type | Notes |
|---|---|---|
rule_id | string | Matches the engine rule id. |
rule_name | string | Falls back to cart_name or "Discount" when the engine hasn’t populated a label yet. |
discount_type | string | tiered, bulk, x_for_y, mix_match, etc. |
percent | number | 0 for fixed-amount discounts. |
amount | string | Minor units (cents/pence/fils), following the Store API convention. JPY=×1, GBP/USD=×100, BHD=×1000. |
is_virtual | boolean | Always true for Dino auto-applied coupons. |
chip_config — per-virtual-coupon display config keyed by the full internal coupon code. The code is the verbatim string CartCoupons::COUPON_PREFIX + rule_id — i.e. dino_dd_<rule_id> — not a user-controlled identifier. Example: a rule with id rule-abc produces the key dino_dd_rule-abc. Used by the registerCheckoutFilters.couponName JS filter to decide between hiding the chip (mode: "hidden") or rendering the human label (mode: "show_name"). See includes/Cart/CartCoupons.php for the canonical definition.
Graceful no-op
Section titled “Graceful no-op”If WooCommerce Blocks / Store API is not available (Automattic\WooCommerce\StoreApi\StoreApi class missing, WC < 7.6), 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 stable within a major version and are referenced throughout this doc per endpoint.