WooCommerce Sequential Order Numbers Pro compatibility
Use this skill when a WooCommerce plugin must cooperate with Sequential Order Numbers Pro (SONP). The core rule is simple: the WooCommerce order ID remains the database primary key; the sequential order number is the human-facing business number.
Mental model
- Treat
$order->get_id()as the immutable internal identifier. - Treat
$order->get_order_number()as the display/order-document number. - Do not build UI, emails, PDFs, CSVs, invoices, ERP payloads, or customer
messages from
#{$order_id}when SONP may be active. - Do not parse the formatted number back into an ID. It may have prefixes, suffixes, date/time tokens, leading zeroes, or a free-order identifier.
- Do not write
_order_number*meta directly unless you are repairing data in a controlled migration.
Plugin contract
SONP 1.21.9 stores and uses these internal fields:
_order_number: numeric sequential portion for normal orders._order_number_formatted: complete visible order number._order_number_free: separate sequence for skipped/free orders._order_number_meta: internal snapshot of format settings; do not depend on this as an integration contract.
Relevant options include woocommerce_order_number_start,
woocommerce_order_number_prefix, woocommerce_order_number_suffix,
woocommerce_order_number_length, woocommerce_order_number_skip_free_orders,
woocommerce_free_order_number_prefix, woocommerce_free_order_number_start,
and the woocommerce_order_number_current / woocommerce_order_number_free_current
performance-mode counters.
Relevant public surfaces:
$order->get_order_number()via thewoocommerce_order_numberfilter.wc_seq_order_number_pro()->find_order_by_order_number( $number ).wc_seq_order_number_pro()->set_sequential_order_number( $order )for rare programmatic-order repair after the order exists and is not a checkout draft.wc_seq_order_number_pro()->format_order_number(...)when building previews, not when reading an existing order.
Extension filters: wc_sequential_order_numbers_formatted_order_number,
wc_sequential_order_numbers_is_free_order,
wc_sequential_order_numbers_performance_mode, and
wc_sequential_order_numbers_generate_sequential_order_number_query.
Detection and display
Check after plugins are loaded:
$sonp_active = function_exists( 'wc_seq_order_number_pro' );
Avoid requiring plugin files manually. Use this everywhere humans see an order number:
$order = wc_get_order( $order_id );
if ( $order instanceof WC_Order ) {
$display_number = $order->get_order_number();
}
Audit account/admin pages, emails, PDFs, invoices, REST responses, webhooks, exports, ERP/CRM payloads, support tools, order tracking forms, and public shortcodes.
Keep both values when syncing externally:
[
'order_id' => $order->get_id(),
'display_order_number' => $order->get_order_number(),
]
Use order_id for idempotency and internal joins; use display_order_number
for customer-facing references.
Lookup by customer-entered order number
Prefer SONP's helper when available:
function myplugin_find_order_id_by_display_number( string $number ): int {
$number = ltrim( trim( $number ), '#' );
if ( function_exists( 'wc_seq_order_number_pro' ) ) {
return (int) wc_seq_order_number_pro()->find_order_by_order_number( $number );
}
$order = wc_get_order( $number );
return $order instanceof WC_Order ? $order->get_id() : 0;
}
If writing your own fallback search, use wc_get_orders() and
_order_number_formatted, not WP_Query over shop_order posts:
$ids = wc_get_orders( [
'return' => 'ids',
'limit' => 1,
'meta_query' => [
[
'key' => '_order_number_formatted',
'value' => ltrim( trim( $number ), '#' ),
],
],
] );
Do not strip non-digits unless the UX explicitly searches only the numeric portion. Prefixes and suffixes are valid order-number data.
The helper can fall back to native order IDs for legacy/unassigned orders. If
the workflow must match only visible SONP numbers, query _order_number_formatted
directly.
Creating orders programmatically
Use WooCommerce CRUD and normal lifecycle hooks. SONP assigns numbers on
checkout update, woocommerce_new_order, status changes out of draft,
admin-created orders, REST insert hooks, and WooCommerce Deposits-created
orders.
Avoid reading the order number too early in the same request. If your plugin needs it immediately after creation, run later than SONP or after the order has been saved:
add_action( 'woocommerce_new_order', function ( $order_id ) {
$order = wc_get_order( $order_id );
if ( $order instanceof WC_Order ) {
$number = $order->get_order_number();
}
}, 20 );
Checkout Block orders may exist as checkout-draft / wc-checkout-draft; do
not force a number onto drafts.
If a legacy importer creates a real order and bypasses all Woo hooks, call:
if ( function_exists( 'wc_seq_order_number_pro' ) ) {
wc_seq_order_number_pro()->set_sequential_order_number( $order );
}
Only do this after the order has an ID and a non-draft status. Never call it on cloned orders before clearing copied SONP meta.
HPOS rules
- Use
WC_OrderCRUD methods andwc_get_orders(). - Do not query
$wpdb->postmetafor order numbers. - Do not assume
shop_orderposts exist for every order. - Do not sort/search admin orders with raw
requestfilters only; HPOS order screens use their own list-table/query filters.
Add admin search via WooCommerce order queries or HPOS-aware filters.
Subscriptions and copied orders
Subscriptions are not normal orders. SONP deliberately removes its meta when an
order is copied to a WC_Subscription, and renewal orders receive their own
sequential order numbers.
When cloning, renewing, splitting, importing, or resubscribing orders, exclude:
$sonp_meta = [
'_order_number',
'_order_number_formatted',
'_order_number_free',
'_order_number_meta',
];
Duplicated order numbers are a hard accounting and support problem.
Free orders
If woocommerce_order_number_skip_free_orders is yes, free orders can use a
separate sequence and visible prefix. In that mode _order_number may be -1
for sorting while the visible number comes from _order_number_formatted.
Use $order->get_order_number() for display and do not assume _order_number
is always the customer-facing number.
Adjust free-order classification only with
wc_sequential_order_numbers_is_free_order, and keep that filter deterministic.
Custom formatting
Use wc_sequential_order_numbers_formatted_order_number for stable,
merchant-wide or order-specific tokens:
add_filter(
'wc_sequential_order_numbers_formatted_order_number',
function ( string $formatted, string $number, int $order_id, string $prefix, string $suffix ): string {
if ( $order_id <= 0 ) {
return $formatted; // settings preview or no order context
}
return $formatted;
},
10,
5
);
Avoid request-specific values, random values, customer language, or mutable order state unless assignment-time dependence is intentional.
Performance mode
wc_sequential_order_numbers_performance_mode switches generation from a
max-meta scan to a current-number option. Use it only for large stores after
testing imports, deletions, and concurrency behavior.
If performance mode is enabled, do not reset current-number options casually, do not expect deleted latest numbers to be reused, test concurrent programmatic creation, and avoid filtering generation SQL without DB-level review.
Audit checklist
Search the target plugin for $order->get_id(), $order->id, order_id,
#, get_post_meta, update_post_meta, $wpdb->postmeta, shop_order,
_order_number*, woocommerce_order_number, woocommerce_new_order, invoice,
PDF, email, export, webhook, ERP, CRM, sync, tracking, and subscription
copy/renewal logic.
Classify findings:
- Bug: customer-facing output uses internal order ID.
- Bug: search accepts visible order numbers but only calls
wc_get_order(). - Bug: clone/import code copies
_order_number*meta. - Bug: HPOS site uses direct
postmeta/shop_orderqueries. - Risk: external system stores only formatted number as the primary key.
- Risk: order number read before SONP assignment hook has run.
Test matrix
Verify HPOS on/off, normal checkout, Checkout Block draft-to-real order, admin-created order, REST-created order, skipped free order, subscription renewal, formatted-number search with prefix/suffix/zeroes, emails/PDFs, invoices, exports, webhooks, and sync retry paths.
Cross-references
- Use
wc-hpos-compatibilitywhen the target code queries order tables. - Use
wc-order-lifecycle-and-itemswhen the target code creates or mutates orders and order items. - Use
wcs-subscription-hookswhen subscription renewal/copy behavior matters. - Use
wc-rest-api-v4when exposing order numbers through custom REST routes.
References
- Official documentation: https://docs.woocommerce.com/document/sequential-order-numbers/
- Official documentation: https://woocommerce.com/products/sequential-order-numbers-pro/
- Verified source paths:
wp-content/plugins/woocommerce-sequential-order-numbers-pro/woocommerce-sequential-order-numbers-pro.phpwp-content/plugins/woocommerce-sequential-order-numbers-pro/class-wc-seq-order-number-pro.phpwp-content/plugins/woocommerce-sequential-order-numbers-pro/src/REST_API.phpwp-content/plugins/woocommerce-sequential-order-numbers-pro/src/Lifecycle.phpwp-content/plugins/woocommerce-sequential-order-numbers-pro/changelog.txt