Code
A Software Licensing API in the EDD Dialect: a Field Guide
The four endpoints every software licensing API needs — check, activate, deactivate, get_version — with curl examples in the EDD dialect Core Forms speaks.
A software licensing API needs exactly four verbs: check a license, activate it on a site, deactivate it, and serve the next version. That’s the whole surface. Everything else — billing, renewals, dashboards — happens on the server side and never touches your plugin’s update code.
For WordPress products, there’s also a de-facto dialect for those four verbs: the one EDD Software Licensing defined years ago. Enough plugins and boilerplates speak it that it’s effectively the standard. Core Forms implements that dialect at /wp-json/core-forms/v1/, so any client code written against EDD’s API works against a Core Forms licensing server without modification.
I’ve shipped WordPress products since 2009, and I’ve written this client code more times than I’d like. Here’s the field guide I wish I’d had.
The four endpoints of a software licensing API
The four endpoints, and the one job each does:
check_license— is this key valid, and until when?activate_license— bind this key to this site URL.deactivate_license— release that binding.get_version— what’s the latest version, and where’s the download?
On a Core Forms licensing server they live at /wp-json/core-forms/v1/check_license and so on — standard WordPress REST API routes, no custom transport, no SDK required. Every request sends the same three things: license (the key), item_id or item_name (which product), and url (the site making the request).
That’s the entire license key API contract. Four routes, three parameters. Let’s walk each one.
check_license: the read-only heartbeat
check_license answers one question — is this key currently valid for this product on this site — without changing anything. It’s the call your plugin makes on a schedule to decide whether to show “license active” or “license expired” in its settings screen.
curl -X POST https://example.com/wp-json/core-forms/v1/check_license \
-d "license=CF-XXXX-XXXX-XXXX" \
-d "item_id=42" \
-d "url=https://client-site.com"
The response is JSON along these lines: a license status field (valid, expired, inactive, invalid), an expires date, and activation counts. Your client cares about exactly one branch: is the status valid or not. Everything else is display data for the settings screen.
Because it’s read-only, check_license is safe to call as often as you like. It never consumes an activation slot. If you’re debugging a customer’s “my license won’t work” ticket, this is the first call you make.
activate_license and deactivate_license: the site binding
Activation binds a license key to a specific site URL; deactivation releases it. This pair is what makes activation limits work — a 3-site license means three distinct URLs can hold an active binding at once.
Activate:
curl -X POST https://example.com/wp-json/core-forms/v1/activate_license \
-d "license=CF-XXXX-XXXX-XXXX" \
-d "item_id=42" \
-d "url=https://client-site.com"
Deactivate is the same request against /deactivate_license. The response tells you whether it worked (license: valid on success) plus how many activation slots are used, along those lines.
Two behaviors worth knowing. First, activating the same URL twice doesn’t burn a second slot — the binding is per-URL, so a reinstall on the same domain just re-confirms the existing activation. Second, when a customer hits their limit, the fix is deactivating an old site, not buying support tickets. On a Core Forms server the customer can see and manage activated sites themselves from the license dashboard — I covered the full lifecycle in how license activation works.
Wire deactivate_license into your plugin’s “deactivate license” button and, ideally, its uninstall routine. Orphaned activations are the number one cause of “I can’t activate on my new site” emails.
get_version: the update half
get_version is what turns a license server into an update server. Your plugin asks “what’s the newest version of item 42, and here’s my license” — the server answers with the latest version number, changelog, and a download package URL if the license is valid.
curl -X POST https://example.com/wp-json/core-forms/v1/get_version \
-d "license=CF-XXXX-XXXX-XXXX" \
-d "item_id=42" \
-d "url=https://client-site.com"
Conceptually the client side is simple: compare the returned version against the installed one, and if it’s newer, hand WordPress the package URL through the update_plugins transient. WordPress does the rest — the update shows up on the Updates screen like any wordpress.org plugin.
The interesting part is what the server does with that package URL. Core Forms doesn’t hand out a permanent link to your ZIP. Download links are signed and expire after 15 minutes, every download is audited, and the ZIP itself is SHA-256-verified against the checksum recorded at upload before it’s served. If the file in the Media Library was tampered with or corrupted, the download refuses rather than shipping a bad build to a thousand sites.
That’s the part most hand-rolled licensing servers skip, and it’s the part I’d least want to rebuild myself. The licensing feature page covers the full server-side setup.
Legacy ?edd_action= compatibility
The EDD Software Licensing API predates the WordPress REST API, so its original transport was query args against the store’s home URL: https://example.com/?edd_action=check_license&license=...&item_id=42&url=.... Thousands of shipped plugins still make requests in exactly that shape, because that’s what EDD Software Licensing documented and what every boilerplate copied.
Core Forms answers those legacy requests too. Same four actions, same parameters, same response shape — just the old query-arg envelope instead of a REST route.
Why this matters: if you’re moving an existing product from an EDD store to a Core Forms licensing server, the plugins already installed on customer sites keep working. You point the API URL constant at the new server and ship nothing. The alternative — forcing every customer to manually update before their updater works again — is a chicken-and-egg problem you don’t want.
For new client code, use the REST routes. They’re cleaner and they’ll play nicely with anything else on the REST and webhooks surface.
Wiring it into a plugin
The standard client pattern is three pieces: a settings field for the key, an activate call on save, and a cached heartbeat.
Settings field. One text input in your plugin’s settings, stored with update_option(). Mask it on display if you like; it’s a license key, not a password.
Activate on save. When the user saves a key, call activate_license immediately and store the result. Show the status right there — “active until 2027-03-01” beats a silent save.
$response = wp_remote_post( 'https://example.com/wp-json/core-forms/v1/activate_license', array(
'body' => array(
'license' => trim( $license_key ),
'item_id' => 42,
'url' => home_url(),
),
) );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
// $data['license'] === 'valid' means you're in business.
Cached heartbeat. A daily WP-Cron event that calls check_license and caches the status in an option or transient. Never call the software licensing API on every page load — one check a day is plenty, and your customers’ sites shouldn’t slow down because your license server had a bad minute. Fail open on network errors: an unreachable server should never lock a paying customer out.
The get_version hook-up rides the same cadence: piggyback it on the update-check filters WordPress already fires, compare versions, inject the package.
The server side of all this — products, plans, checkout, fulfillment — is a separate topic; the commerce setup docs walk through it end to end. One honest boundary note: the payment gateway still owns recurring charges and stored payment methods, and Core Forms isn’t a merchant of record, so global sales tax is yours to handle.
FAQ
Is the Core Forms licensing API really EDD-compatible?
Yes, in both directions that matter: the REST routes at /wp-json/core-forms/v1/ accept the same license, item_id/item_name, and url parameters EDD Software Licensing defined, and legacy ?edd_action= query-arg requests from already-shipped clients work unchanged. Existing EDD updater classes point at it without code changes.
How often should my plugin call check_license?
Once a day via WP-Cron, cached in an option. The check is read-only and cheap, but there’s no reason to call it more often — license status changes rarely. Never check on every page load, and treat network failures as “keep last known status,” not “deactivate.”
What happens when a customer hits their activation limit?
activate_license returns a non-valid status and the activation counts, so your plugin can show a useful message. The customer frees a slot by deactivating an old site — on a Core Forms server they can do that themselves from the customer license dashboard, no support ticket needed.
Do I need authentication keys for these endpoints?
No. The license key itself is the credential — every request carries license, the product identifier, and the requesting url, and the server validates that combination. That’s the EDD convention, and it’s why shipped clients work across any server speaking the dialect.