Launch pricing: Lock in Pro at $79/yr before prices go up View pricing →

Developers & integrations

Overview

Expiration dates are fully scriptable. External systems — a POS, an ERP, a barcode-scanner workflow, Zapier/Make automations, or your own code — can read and write dates through the WooCommerce REST API, manage them from WP-CLI, and subscribe to expiry events through signed webhooks.

Every programmatic write goes through the same central save path as the admin screens: variable products resolve correctly, catalog visibility stays in sync, and every change is recorded in the activity log with its source.

REST API

The edfw_expiration_date field

Products and variations on the WooCommerce REST API (wc/v3) carry an edfw_expiration_date field — visible in the endpoint schema (OPTIONS /wp-json/wc/v3/products), so schema-driven clients discover it automatically.

// GET /wp-json/wc/v3/products/123
{
  "id": 123,
  "name": "Organic Whole Milk",
  "edfw_expiration_date": "2026-08-01"
}

Reading: the field is YYYY-MM-DD in the site timezone, or an empty string when no date is set. A variable product exposes its inherited default (the value the product editor edits); each variation exposes its own date.

Writing: include the field in any create or update request — single-item routes, the /batch endpoints, and the legacy wc/v1 namespace all work:

curl -X PUT https://example.com/wp-json/wc/v3/products/123 \
  -u ck_xxx:cs_xxx \
  -H "Content-Type: application/json" \
  -d '{"edfw_expiration_date": "2026-08-01"}'
  • An empty string or null clears the date.
  • A malformed or out-of-range date is rejected with a 400 error (edfw_rest_invalid_expiration_date) before anything is saved — on batch routes the error is reported per item. Dates are never silently dropped.
  • Writing to a variable product sets its inherited default; writing to a variation sets that variation’s own date and re-syncs the parent.
  • Every change appears in the activity log as “API: date set / cleared” with the acting API user recorded.

Filtering by expiring window

The products collection accepts two extra query parameters, so you can pull everything expiring in a window in one call:

# Everything expiring within the next 14 days
curl "https://example.com/wp-json/wc/v3/products?edfw_expires_after=2026-07-04&edfw_expires_before=2026-07-18&per_page=100" \
  -u ck_xxx:cs_xxx
ParameterMeaning
edfw_expires_beforeOnly products whose date is on or before this YYYY-MM-DD date
edfw_expires_afterOnly products whose date is on or after this YYYY-MM-DD date

Both combine into a range. Variable products match once, by their earliest variation date — the same semantics as the admin report, so counts agree across every surface.

WP-CLI

The wp edfw command manages dates from the shell — handy for hosting automation, cron scripts, and bulk operations.

wp edfw get 123                 # print a product's date (or "(none)")
wp edfw set 123 2026-12-31      # set a date (validated; errors cleanly)
wp edfw clear 123               # clear a date

# List dated products, optionally within a window
wp edfw list --expires-before=2026-08-01 --format=table
wp edfw list --expires-before=2026-08-01 --format=count
wp edfw list --expires-before=2026-08-01 --format=csv > expiring.csv

# Formats compose: clear every date expiring before a cutoff
wp edfw clear $(wp edfw list --expires-before=2026-07-10 --format=ids)

list shows one row per product (a variable product appears once, with its earliest variation date) with ID, name, sku, expiration, and days_left columns; --format accepts table, csv, json, ids, and count. Writes are logged in the activity log with source wp_cli.

Event webhooks (Pro)

The Pro add-on can POST a JSON payload to a URL you configure whenever an expiry event happens — a product set out of stock or hidden by the sweep, stock written off, a variation or batch expiring, or the cleanup tool moving a product to the trash. One URL is enough to reach Slack (incoming webhooks), Zapier, Make, or your own endpoint.

Configure it under Products → Expirations → Settings → Notifications → Event webhook.

Payload

A single event:

{
  "event": "written_off",
  "label": "Stock written off",
  "message": "Remaining stock written off from the Overview quick actions (8 units, $80.00)",
  "text": "Remaining stock written off from the Overview quick actions (8 units, $80.00)",
  "product_id": 123,
  "product_name": "Organic Whole Milk",
  "sku": "OWM-123",
  "data": { "qty": 8 },
  "log_id": 4512,
  "site": "https://example.com",
  "timestamp": "2026-07-04 05:00:00"
}

Events that happen in a burst (for example a daily sweep expiring many products at once) are batched into a single request so your endpoint is never flooded:

{
  "event": "edfw_batch",
  "text": "14 expiry events on your store — see the events array or the Activity Log.",
  "count": 14,
  "site": "https://example.com",
  "events": [ { "event": "set_out_of_stock", "product_id": 55, "...": "..." } ]
}

The text key makes payloads work as-is with Slack incoming webhooks. The X-EDFW-Event header carries the event type (edfw_batch for bursts). Delivery is non-blocking and best-effort — a slow endpoint can never stall your store.

Verifying signatures

Set a signing secret and every request carries an X-EDFW-Signature header: base64-encoded HMAC-SHA256 of the raw request body. Verify it before trusting a payload:

$body      = file_get_contents( 'php://input' );
$signature = $_SERVER['HTTP_X_EDFW_SIGNATURE'] ?? '';
$expected  = base64_encode( hash_hmac( 'sha256', $body, 'your-secret', true ) );

if ( ! hash_equals( $expected, $signature ) ) {
    http_response_code( 401 );
    exit;
}
// Node.js
const crypto = require('crypto');
const expected = crypto.createHmac('sha256', 'your-secret').update(rawBody).digest('base64');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-edfw-signature'] || ''));

Forwardable event types: set_out_of_stock, hidden_from_catalog, written_off, variation_expired, batch_expired, cleanup_trashed (filterable via edfw_webhook_action_types).

Extension hooks

A few stable extension points for your own code:

HookTypePurpose
edfw_activity_loggedactionFires after every activity-log insert: ($log_id, $product_id, $action_type, $message, $data) — a generic event stream
edfw_effective_expiry_policyfilterOverride the expiry behaviour per product
edfw_admin_tabs / edfw_render_tabfilterRegister a top-level tab on the Expiration Dates page
edfw_show_onboardingfilterForce the getting-started card on or off
edfw_digest_include_analyticsfilterRemove the analytics section from digest emails
edfw_writeoff_unit_valuefilterSubstitute cost-of-goods for the regular price in waste valuation

Good to know

  • Programmatic writes are validated with the same rules as the admin: YYYY-MM-DD, years 2000–2100.
  • Avoid writing the raw _edfw_expiration_date meta via generic meta_data — it bypasses validation and syncing. Use the first-class field.
  • Every write surface (REST, CLI, CSV import, admin) records its source in the activity log, so you can always trace where a date came from — and the log itself is exportable as CSV.