WPMake

← Back to BlogHow to Let Guests Track Their WooCommerce Order Without an AccountTUTORIAL

How to Let Guests Track Their WooCommerce Order Without an Account

⏱ 8 min read  ·  👤 iamprazol  ·  Sep 5, 2026

Guest checkout is good for conversion and terrible for everything that happens after the sale. Your guest customers have no account, so they have no orders page, so the only route they have to find out what is happening is you.

There is a fix, and there is a way of building that fix which quietly turns your store into a customer-data lookup service. This guide covers both.

Start with what core already gives you

WooCommerce ships a shortcode for exactly this:

[woocommerce_order_tracking]

Put it on a page. The customer enters an order number and the billing email, and if both match they see the order’s status and items. The implementation is in WC_Shortcode_Order_Tracking, and it is worth knowing exactly what it checks — it loads the order, then compares the submitted email against $order->get_billing_email(), lowercased on both sides. No match, no order.

That is a real security control and it is already there. Before you install anything, put this shortcode on a page and link it from your footer. It costs nothing.

Its three real limitations

  • It is status-only. The customer sees the same one-word status they would have seen in an account, with the items below it. No stages, no dates, no expectation of when anything will arrive.
  • There are no actions. Having found their order, the customer can do nothing with it. Not cancel it, not reorder it, not ask about it with the order attached. They still have to email you — you have moved the ticket, not removed it.
  • It is a bare form on a page. It does not appear in your emails, so the customer has to know the page exists, remember their order number, and go looking. Most will email you instead because that is fewer steps.

None of that makes it useless. It makes it a floor, not a ceiling.

The security problem nobody mentions

Here is the part that matters, and the reason this post exists.

WooCommerce order IDs are sequential. Order 1041 is followed by order 1042. Anyone who has ever bought from you knows one valid order number, and can therefore guess several thousand more.

Now consider the lookup form somebody builds when they decide core’s shortcode is too plain. It takes an order number. It looks the order up. If it finds one, it shows the order. If it does not, it says “order not found”.

That form is an enumeration oracle. A script walks the number range and gets back, for each valid order, whatever the page renders — which on most implementations is the customer’s name, their delivery address, what they bought and what they paid. Nobody has to break anything. The form is working exactly as built.

The subtler version has an email field but leaks anyway. If a wrong email returns “that email does not match this order” and a non-existent order returns “order not found”, the two different messages tell an attacker which order numbers are real. Distinguishable responses are the leak, even when the data itself is protected.

What a safe implementation requires

Four things, and you need all four.

1. The email must match, always

An order number on its own is never sufficient. Compare after normalising — trim and lowercase both sides — and compare with hash_equals() rather than === so the comparison does not leak timing information.

2. Identical responses, whatever the failure

Wrong email, non-existent order, right order but a typo — all three produce the same page. The pattern that works well is to make the response a non-answer that is also useful:

If that order exists, we have emailed a secure link to the address on file for it.

That sentence is true whether or not the order exists. It reveals nothing. And when the order does exist, the email genuinely goes out — so the legitimate customer gets what they came for and the ticket is still deflected.

A guest order lookup form asking for an order number and billing email, with a button reading Email me a secure link
The form promises an email, not a page. That single design decision removes the oracle.

3. Rate limiting on more than one axis

Per IP is the obvious one and the easiest to work around. Limit per IP, per email address, and per site. Something in the region of five attempts per quarter hour per IP and ten per hour per email is strict enough to make enumeration pointless and loose enough that a confused customer never notices.

Store the counters in transients or the object cache, not in wp_options. Options have no expiry and autoload by default, so a rate limiter built on them slowly poisons every page load on the site.

4. The email goes to the address on the order

Never to the address typed into the form. If a submitted address can receive the link, your form is a mail-bomb tool with your domain in the From header, and the first person to notice will be your deliverability provider.

Signed links are the better primary route

A lookup form is a fallback for a customer who has lost their email. The main route should be a link in the transactional email they already received.

A signed link carries a token that identifies the order and proves the link came from you. A reasonable construction:

payload = order_id | order_key | expiry
token   = base64url( payload ) . '.' . hash_hmac( 'sha256', payload, $secret )

Four rules make this safe.

  • Verify with hash_equals(). A plain string comparison on an HMAC is a timing oracle.
  • Include the order key. It gives you a revocation path — rotate an order’s key and every outstanding link for that order dies, which is what you want the day a customer forwards one to the wrong person.
  • Give it a sane lifetime. Two weeks is a reasonable default; anything past a couple of months is a token you have lost track of.
  • Make it idempotent within its life. Do not burn a token on first use. Corporate mail scanners and chat link previewers fetch URLs before a human ever clicks, so a one-time token means your customer’s very first click lands on a dead link.

The rule that matters most: never log them in

A signed link must grant access to one order. It must never call wp_set_auth_cookie() or otherwise create a WordPress session.

Magic-login links look convenient and are an account-takeover primitive. That URL will end up in browser history on a shared laptop, in a forwarded email, in a referrer header, in a proxy log. If it grants an order view, the worst case is that somebody sees one order. If it grants a session, the worst case is that somebody has the customer’s account, their address book and their saved payment methods.

The related habit: get the token out of the URL as soon as it has been used. Land on the token, exchange it for a short-lived cookie-bound context, redirect to a clean URL. After that first hop the token is not in the address bar, not in the referrer sent to any third-party script on the page, and not in your access logs.

The Guest Access settings screen with the lookup toggle, a security acknowledgement toggle and a secure link lifetime in days
Guest lookup is a public endpoint. It should be something you switch on deliberately, not something an install turns on for you.

Caching will serve one customer’s order to another

This one is not theoretical, and it is the reason to test on a staging site that has your real caching stack on it.

A guest order page has no login cookie. To a page cache it is an anonymous page like any other, so it gets cached — and the next anonymous visitor to that URL is served the previous customer’s name, address and order contents from cache.

Three defences, and use all of them:

  • Call wc_nocache_headers() on every order-bearing response, and define DONOTCACHEPAGE — most WordPress caching plugins honour it.
  • Send Cache-Control: private, no-store explicitly. A CDN in front of your host has never heard of your caching plugin’s constant.
  • Never put order data in a GET query string. Query strings end up in access logs, in analytics, and in CDN cache keys.

Test it properly: place a guest order, open its link, then open the same URL in a private window from a different network. If you see the order, you have a leak.

What good looks like

Working setups tend to have all four of these:

  • A signed link in every transactional email, so most guests never look anything up.
  • A lookup form as a fallback, which emails a link rather than rendering the order.
  • Rate limits on both, with a generic throttle message.
  • The same order view guests and account holders see, so there is one page to maintain and one thing to explain.

And once a guest can see their order, the obvious next question is what they can do with it — cancelling and the wider guest post-purchase gap are where that goes next.

The limitation worth stating

Everything above is about access, not information. A guest who successfully finds their order still only learns what your order data knows. If your statuses are updated in a Friday batch and you have no tracking numbers, a guest lookup shows them a vague answer faster — which is an improvement, but a smaller one than it looks.

Fix the underlying information first, then make it reachable.

How to Let Guests Track Their WooCommerce Order Without an Account