Designing a License API for Commercial WordPress Plugins
We've written before about how plugin licensing looks from the customer's side — keys, activation limits, renewals. This post is the other half: what the server API behind that experience actually needs to do. It's drawn from the licensing API we run for our own plugin catalog, including a design pitfall we found in our own implementation that's easy to miss and worth designing out from day one.
The three verbs a license API actually needs
Strip away the dashboards and a plugin license API reduces to three operations. Activate binds a license to a site, consuming one of the license's activation slots. Deactivate releases that binding — essential for migrations, or customers hit an activation ceiling they can't clear themselves and your support inbox pays for it. Lookup returns the license's current state (plan, status, activations, support expiry) so the customer portal and the plugin's settings screen can show something truthful.
Authentication for all three is the license key plus the purchase email. Neither alone is enough: keys leak into config backups and screenshots, and an email address is public. The pair, plus aggressive rate limiting, gives you a workable trust model without forcing WordPress site owners through OAuth.
Heartbeats are not lookups — keep them separate
The plugin itself needs to answer one recurring question: "is this license still valid?" That check runs on a schedule from thousands of installed sites, so it deserves its own endpoint with its own shape — a minimal {valid, status, supportActive} response rather than the full license object. Two reasons beyond tidiness:
Different rate profiles. A heartbeat endpoint gets steady automated traffic; the lookup endpoint gets bursty human traffic. Separate endpoints mean separate rate limits, so a fleet of sites checking in can't lock a real customer out of their portal.
Different failure semantics. A heartbeat should degrade gracefully — a plugin that can't reach your server should keep working on its cached answer for a grace window, not disable itself because your host had a bad afternoon. Designing the endpoint response to be small and cacheable makes that client-side behavior natural to build.
The cross-plugin pitfall: scope activations to the plugin
Here's the design flaw we found in our own system, and the reason this post exists. If you sell multiple plugins through one licensing API, and your activation logic dedupes by site URL alone, then a license for Plugin A can silently activate Plugin B on the same site. The activation record says "this license is active on shop.example.com" — and nothing in the activate call says which product is asking.
The fix is structural, not a patch: every activation, deactivation, and heartbeat request must carry the plugin's identity, and the server must reject any request whose plugin doesn't match the plugin the license was sold for. One license, one product — enforced server-side, because the client (a plugin on a site you don't control) can claim to be anything. After the fix, we pinned the behavior with contract tests that assert the wrong-plugin request fails, so the check can't quietly regress in a refactor.
If you're designing fresh: put the product identifier in the request schema from the first version. Retrofitting it means updating every deployed client at once, which is exactly as fun as it sounds.
Brute-force pressure is different for license APIs
License lookup endpoints get probed — key formats are guessable in shape, and attackers enumerate. Two layers have worked well for us. A fixed-window rate limit per IP caps raw request volume. On top of that, tracking consecutive authentication failures per IP separately lets you log and alert on someone methodically guessing key/email pairs at a rate below your hard limit — signal you'd otherwise never see. Failed-attempt tracking resets on any success, so a customer who typos their email twice isn't treated like an attacker.
One rule worth stating plainly: license keys belong in request bodies or headers, never in query strings, where they'd persist in every proxy and access log between the customer's site and yours.
What the API contract owes your deployed plugins
The hardest constraint on a license API isn't security — it's that your clients are plugins installed on other people's servers, updating on their schedule, not yours. A renamed response field that would be a one-line frontend fix on a normal web app is a breaking change across every customer site simultaneously. Treat the request/response shapes as a versioned contract: pin them with tests that assert the exact field names your shipped clients read, and when you must change the shape, support both forms until update telemetry says the old clients are gone.
That contract discipline is also what makes the rest of the commercial machinery — update manifests, renewal reminders, activation limits — safe to iterate on server-side without touching deployed code.
Build vs. borrow
None of this is exotic engineering, but it is a real system with real failure modes, PII to protect, and a support burden when it misbehaves. If you're taking a plugin commercial and would rather ship features than licensing plumbing, our WordPress plugin development team has built this stack end to end — talk to us about what your licensing model actually needs before you commit to building all of it yourself.