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
nullclears 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
| Parameter | Meaning |
|---|---|
edfw_expires_before | Only products whose date is on or before this YYYY-MM-DD date |
edfw_expires_after | Only 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:
| Hook | Type | Purpose |
|---|---|---|
edfw_activity_logged | action | Fires after every activity-log insert: ($log_id, $product_id, $action_type, $message, $data) — a generic event stream |
edfw_effective_expiry_policy | filter | Override the expiry behaviour per product |
edfw_admin_tabs / edfw_render_tab | filter | Register a top-level tab on the Expiration Dates page |
edfw_show_onboarding | filter | Force the getting-started card on or off |
edfw_digest_include_analytics | filter | Remove the analytics section from digest emails |
edfw_writeoff_unit_value | filter | Substitute 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_datemeta via genericmeta_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.