
September 08, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
Store owners often need WooCommerce Coupons and Discounts Programmatically when manual admin work breaks down. Flash sales, loyalty tiers, and partner codes do not scale through the dashboard alone. On florist stores like Petals Nepal and other WooCommerce builds, I regularly wire discounts into checkout logic rather than asking staff to create hundreds of codes by hand. This guide covers coupon creation, cart application, validation hooks, bulk generation, and REST endpoints for WordPress 7.1 with WooCommerce 11.1.
WC_Coupon class or REST API, then apply them via WC()->cart->apply_coupon() or custom woocommerce_cart_calculate_fees hooks when you need rule-based pricing without a code.How do you create WooCommerce coupons programmatically in PHP?
WooCommerce stores each coupon as a custom post type called shop_coupon. You can insert that post directly, but the cleaner path is the WC_Coupon object. It wraps meta fields WooCommerce expects and keeps your code readable.
Step-by-step coupon creation
- Ensure WooCommerce is loaded before you call coupon classes.
- Instantiate
WC_Couponwith a unique code string. - Set discount type, amount, usage limits, and product restrictions.
- Call
save()to persist the coupon post and meta. - Verify the coupon in admin or via a test cart apply.
Place one-off scripts in a must-use plugin or an admin-only tool page. Never leave unauthenticated public endpoints that mint coupons.
<?php
/**
* Create a single-percentage coupon programmatically.
* Requires WordPress 7.1 + WooCommerce 11.1.
*/
function myshop_create_percent_coupon( string $code, float $amount ): int {
if ( ! class_exists( 'WC_Coupon' ) ) {
return 0;
}
$coupon = new WC_Coupon();
$coupon->set_code( sanitize_text_field( $code ) );
$coupon->set_discount_type( 'percent' );
$coupon->set_amount( $amount );
$coupon->set_individual_use( true );
$coupon->set_usage_limit( 1 );
$coupon->set_usage_limit_per_user( 1 );
$coupon->set_date_expires( strtotime( '+30 days' ) );
$coupon->set_free_shipping( false );
return $coupon->save();
}
// Example: 15% off, single use, expires in 30 days.
myshop_create_percent_coupon( 'DASHAIN15', 15 ); Supported discount_type values include percent, fixed_cart, and fixed_product. Fixed-product coupons apply per matching line item. Fixed-cart coupons subtract a flat amount from the order subtotal after product-level rules run.
Product and category restrictions
Restrict coupons to specific SKUs or categories with array meta setters. This pattern works well for category-wide promos on large catalogs.
$coupon->set_product_ids( [ 101, 205, 309 ] );
$coupon->set_excluded_product_ids( [ 888 ] );
$coupon->set_product_categories( [ 12, 18 ] );
$coupon->set_exclude_sale_items( true );
$coupon->save(); On multi-currency stores, pair coupon logic with proper NPR localization settings so fixed amounts match the active currency. A Rs 500 flat discount and a USD 5 discount are not interchangeable without explicit currency handling.
The official WC_Coupon class reference documents every setter. Cross-check field names against the WooCommerce coupon management guide before you ship production scripts.
How do you apply a WooCommerce discount to the cart without the customer entering a code?
Not every promotion needs a visible code. Loyalty discounts, first-order rewards, and cart-threshold deals often work better as automatic rules. WooCommerce gives you two main paths.
Auto-apply an existing coupon
If the coupon already exists, hook into woocommerce_before_calculate_totals or woocommerce_cart_loaded_from_session and call apply_coupon() when your business rule matches.
add_action( 'woocommerce_cart_loaded_from_session', 'myshop_auto_apply_loyalty_coupon' );
function myshop_auto_apply_loyalty_coupon(): void {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
$user_id = get_current_user_id();
if ( ! $user_id || ! wc_coupons_enabled() ) {
return;
}
$order_count = wc_get_customer_order_count( $user_id );
if ( $order_count < 3 ) {
return;
}
$code = 'LOYAL3';
if ( ! WC()->cart->has_discount( $code ) ) {
WC()->cart->apply_coupon( $code );
}
} Guard against infinite recalculation loops. Only apply when the cart lacks the code, and avoid calling apply_coupon() on every micro-interaction without a condition.
Cart fees for custom discount logic
When no coupon post fits the rule, use negative fees. This approach powers tiered cart discounts and bundle pricing without polluting the coupon list.
add_action( 'woocommerce_cart_calculate_fees', 'myshop_cart_threshold_discount', 20, 1 );
function myshop_cart_threshold_discount( WC_Cart $cart ): void {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
$subtotal = (float) $cart->get_subtotal();
if ( $subtotal < 5000 ) {
return;
}
$discount = round( $subtotal * 0.10, 2 );
$cart->add_fee( 'Cart discount (10%)', -$discount, false );
} Negative fees show as line items in cart and checkout. They do not increment coupon usage counters. That matters when you track redemption limits for accounting.
For checkout UX work beyond discounts, see custom checkout fields with validation. Pair automatic discounts with clear order summaries so customers trust the math.
Which WooCommerce hooks control coupon validation and discount amounts?
Core coupon math runs through a filter chain. Hook into the right stage and you can enforce domain rules without forking WooCommerce core.
Validation filters
woocommerce_coupon_is_valid— final yes/no gate before apply.woocommerce_coupon_is_valid_for_product— per line-item eligibility.woocommerce_coupon_error— customize error messages shown to shoppers.
On a legal-tech or booking site, you might block coupons when a mandatory service fee product is in the cart. On international florist stores, I have blocked codes for same-day delivery SKUs where margins are fixed.
add_filter( 'woocommerce_coupon_is_valid', 'myshop_block_coupon_on_express', 10, 3 );
function myshop_block_coupon_on_express( bool $valid, WC_Coupon $coupon, WC_Discounts $discounts ): bool {
if ( ! $valid ) {
return false;
}
foreach ( WC()->cart->get_cart() as $item ) {
$product = $item['data'];
if ( $product && has_term( 'same-day', 'product_cat', $product->get_id() ) ) {
throw new Exception( 'Coupons cannot be used with same-day delivery items.' );
}
}
return $valid;
} Adjusting the calculated discount
Filter woocommerce_coupon_get_discount_amount when you need caps, rounding rules, or category-weighted math. Filter woocommerce_coupon_get_discount_amount_for_cart_item for line-level control.
add_filter( 'woocommerce_coupon_get_discount_amount', 'myshop_cap_percent_discount', 10, 5 );
function myshop_cap_percent_discount( $discount, $discounting_amount, $cart_item, $single, $coupon ) {
if ( 'percent' !== $coupon->get_discount_type() ) {
return $discount;
}
$max = 1000; // Rs 1,000 cap (~USD 7.50 at typical rates).
return min( (float) $discount, $max );
} Test capped coupons against mixed carts. A cap that works for one line item may under-discount bundles unless you adjust for quantity.
Run checkout regression tests after adding validation hooks. A thrown exception on an edge-case cart blocks payment entirely.
How do you bulk-generate coupon codes for a marketing campaign?
Affiliate batches, event handouts, and SMS campaigns need unique codes at scale. Loop WC_Coupon creation with a collision check, or insert posts in chunks during a WP-Cron job.
Batch generator with uniqueness check
function myshop_generate_unique_code( int $length = 8 ): string {
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
do {
$code = '';
for ( $i = 0; $i < $length; $i++ ) {
$code .= $chars[ random_int( 0, strlen( $chars ) - 1 ) ];
}
$id = wc_get_coupon_id_by_code( $code );
} while ( $id );
return $code;
}
function myshop_bulk_create_coupons( int $count, float $amount ): array {
$created = [];
for ( $i = 0; $i < $count; $i++ ) {
$code = myshop_generate_unique_code();
$coupon = new WC_Coupon();
$coupon->set_code( $code );
$coupon->set_discount_type( 'fixed_cart' );
$coupon->set_amount( $amount );
$coupon->set_usage_limit( 1 );
$coupon->save();
$created[] = $code;
}
return $created;
} Export the returned array to CSV for your email tool. Validate output with a JSON formatter if you store batches as API payloads first.
Performance notes for large batches
Creating thousands of coupons in one HTTP request will time out on shared hosting. Batch in groups of 100–250 inside WP-Cron or a CLI script. Pause between batches to keep MySQL write load manageable.
The same batching mindset applies to bulk product CSV imports. Treat coupon generation as a background job, not a synchronous admin click.
Log created codes to a private file outside the web root. Publicly guessable patterns invite brute-force redemption attempts.
Can you manage WooCommerce coupons through the REST API?
Yes. WooCommerce exposes coupons at /wp-json/wc/v3/coupons. This endpoint suits mobile apps, internal admin panels, and CI-driven staging setups.
Create a coupon via REST
POST /wp-json/wc/v3/coupons
Authorization: Basic {consumer_key}:{consumer_secret}
Content-Type: application/json
{
"code": "API20OFF",
"discount_type": "percent",
"amount": "20",
"individual_use": true,
"usage_limit": 100,
"date_expires": "2026-12-31T23:59:59"
} Use read/write API keys with least privilege. Rotate keys after contractor access ends. For broader mobile commerce patterns, read the WooCommerce REST API for mobile apps guide.
Comparison: PHP, REST, and admin UI
| Approach | Best for | Usage tracking | Deployment |
|---|---|---|---|
WC_Coupon in PHP | Theme hooks, auto-rules, WP-Cron jobs | Full native support | mu-plugin or custom plugin |
| REST API | External apps, headless admin, CI seeding | Full native support | HTTP + API keys |
| Negative cart fees | Tiered cart rules, hidden loyalty | Not counted as coupons | Plugin hook only |
| WooCommerce admin UI | Small manual sets | Full native support | No code required |
For most production stores I maintain, PHP hooks handle automatic logic and REST handles partner integrations. The admin UI remains fine for one-off influencer codes.
Deleting and updating coupons safely
Update with $coupon = new WC_Coupon( $id );, change setters, then save(). Delete via wp_trash_post( $coupon_id ) or the REST DELETE verb. Never hard-delete posts without checking pending orders that referenced the code.
Combine programmatic coupons with abandoned cart recovery flows by embedding unique single-use codes in recovery emails. Generate the code when the cart is abandoned, not hours later at click time.
What production pitfalls break programmatic WooCommerce discounts?
Most failures I debug are environmental, not mathematical. They show up after a plugin update or a caching layer change.
- Opcode cache staleness: After deploying coupon logic, reload PHP-FPM so opcache picks up the new plugin file.
- Caching plugins: Full-page cache on cart and checkout bypasses dynamic coupon hooks. Exclude those routes explicitly.
- Individual use conflicts: Auto-applied loyalty codes may reject manually entered affiliate codes. Decide stacking policy upfront.
- Tax display settings: Negative fees may or may not inherit tax class behaviour depending on WooCommerce tax options.
- HPOS compatibility: WooCommerce 11.x with High-Performance Order Storage still stores coupon line items on orders. Test order admin views after major upgrades.
For platform-level context, compare stacks in the Magento vs Shopify vs WooCommerce 2026 article. WooCommerce wins on coupon hook flexibility for PHP teams.
Speed matters when coupon validation runs extra queries. Audit cart performance using ideas from WooCommerce speed optimization for large catalogs. Keep validation logic O(n) over cart items, not per-product database lookups inside nested loops.
If you need custom payment flows alongside discounts, review custom payment gateway development so discounted totals match what gateways charge.
For ongoing store work, WordPress development and support retainers cover the upgrades that break coupon hooks silently. Document every custom filter in your plugin readme so the next developer knows why a cap exists.
Subscription and renewal discounts need separate handling. See subscription setup without paid extensions before you attach recurring coupons that renewals should ignore.
Order emails must reflect applied discounts clearly. Misaligned totals trigger support tickets. Align messaging with custom order notification setup patterns.
I've shipped similar logic across international WooCommerce florists and local NPR stores. The code patterns stay the same; currency and tax rules change.
Read more engineering notes on my background or browse the full blog archive for adjacent WooCommerce topics.
Key Takeaways
- Create coupons with
WC_Couponsetters andsave()rather than raw post inserts unless you have a specific reason. - Auto-apply existing codes with
WC()->cart->apply_coupon(); use negative fees when you do not need redemption tracking. - Hook
woocommerce_coupon_is_validandwoocommerce_coupon_get_discount_amountfor business rules and caps. - Batch-generate unique codes with collision checks inside WP-Cron, not one long admin request.
- Expose create/update flows via
/wp-json/wc/v3/couponsfor external systems with locked-down API keys. - Test mixed carts, sale items, tax settings, and cached checkout routes before launching a campaign.
People Also Ask
What is the difference between a WooCommerce coupon and a cart fee discount?
A coupon is a shop_coupon post with usage limits, expiry, and reporting in WooCommerce analytics. A negative cart fee is calculated at runtime and does not increment coupon redemption counters. Use coupons when marketing needs audit trails; use fees for silent tier pricing.
Can WooCommerce coupons work with sale products?
Only if the coupon’s exclude_sale_items meta is false. Many stores set exclusion to protect margin on already-discounted SKUs. Check this flag programmatically with $coupon->get_exclude_sale_items() before launching a site-wide code.
How do you apply a coupon programmatically during checkout?
Call WC()->cart->apply_coupon( 'CODE' ) before totals calculate, typically on woocommerce_cart_loaded_from_session. Avoid calling it repeatedly without checking has_discount() first, or you risk recalculation loops and PHP timeouts.
Does the WooCommerce REST API support bulk coupon creation?
The REST API creates one coupon per POST request. Bulk workflows loop requests from a script or queue, or use server-side PHP batch generation for better performance. Add throttling so you do not trigger rate limits on managed hosts.
Ship discount logic you can maintain
WooCommerce Coupons and Discounts Programmatically give you full control over promotions without dashboard bottlenecks. Start with WC_Coupon for tracked codes, layer hooks for validation, and reach for cart fees only when reporting requirements allow it. Test on staging with real product types before you push a Dashain or holiday campaign live.
Need custom coupon engines, affiliate batches, or checkout discount rules on a live store? Contact us to discuss your WooCommerce project, or explore e-commerce development services for end-to-end implementation.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

