Skip to main content

Quickstart: Validate a License in 5 Minutes

This page gets you from zero to a working license check. By the end you will have called POST /licenses/validate, read the response, and gated a feature on status == "valid".

You need two things first:

  • A vendor API key (how to get one). Use a ch_vendor_test_ key while developing.
  • A license key to test against — issue a test license from your vendor dashboard, or use one a customer gave you.

Step 1 — Validate with curl​

The validate endpoint takes a license key, the domain you are checking, and optionally the product. It returns the license's status and entitlements.

curl https://api.code-heaven.com/v1/licenses/validate \
-H "X-CH-Vendor-Key: $CH_VENDOR_KEY" \
-H "Content-Type: application/json" \
-d '{
"licenseKey": "CH-7K2P-9XQ4-LM83",
"domain": "example.com",
"product": "acme-forms-pro"
}'

A valid license responds 200:

{
"valid": true,
"status": "valid",
"product": "acme-forms-pro",
"expiresAt": "2027-06-04T00:00:00Z",
"activations": [
{ "domain": "example.com", "activatedAt": "2026-06-04T10:21:00Z" }
],
"seatLimit": 3
}

The two fields you gate on are valid and status. They agree: valid is true exactly when status is "valid". Read status when you want to tell the customer why a check failed.

If the key is real and paid-up but this domain has never been activated, you get valid: false with status: "domain_not_activated" — that is your cue to activate the domain (see the license lifecycle):

{
"valid": false,
"status": "domain_not_activated",
"product": "acme-forms-pro",
"expiresAt": "2027-06-04T00:00:00Z",
"activations": [],
"seatLimit": 3
}

Step 2 — Validate from PHP​

Here is the same call in plain PHP using cURL — no framework, no SDK. Drop it into your plugin or your licensing proxy.

<?php

function ch_validate_license(string $licenseKey, string $domain, string $product): array
{
$payload = json_encode([
'licenseKey' => $licenseKey,
'domain' => $domain,
'product' => $product,
]);

$ch = curl_init('https://api.code-heaven.com/v1/licenses/validate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-CH-Vendor-Key: ' . getenv('CH_VENDOR_KEY'),
],
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$errno = curl_errno($ch);
curl_close($ch);

// Network failure: never silently unlock or hard-lock. Fall back to your
// cached verdict and an offline grace window (see the PHP SDK page).
if ($errno !== 0) {
throw new RuntimeException('License server unreachable: ' . $errno);
}

$data = json_decode($body, true) ?: [];

if ($status === 401) {
// Your problem, not the customer's: bad or missing vendor key.
throw new RuntimeException('Vendor key rejected (401).');
}

return $data;
}

Step 3 — Gate the plugin on status == valid​

The whole point is this one decision. Premium features run only when the license validates.

<?php

function ch_premium_unlocked(): bool
{
$domain = parse_url(home_url(), PHP_URL_HOST); // current site domain

try {
$result = ch_validate_license(
get_option('acme_license_key', ''),
$domain,
'acme-forms-pro'
);
} catch (RuntimeException $e) {
// Server unreachable or vendor-key error. Use your cached verdict
// rather than punishing the customer for your downtime.
return ch_cached_verdict_or_grace();
}

return ($result['status'] ?? '') === 'valid';
}

// Usage in your plugin:
if (ch_premium_unlocked()) {
acme_register_premium_features();
} else {
acme_show_license_notice(); // prompt the customer to enter/renew a key
}

Gate on status == "valid", not merely on a 200 response. A 200 means the API answered; the answer might be expired or revoked. Always inspect status.

What to do for each status​

statusMeaningWhat your plugin should do
validPaid, active, this domain is activatedUnlock premium features
invalidKey is wrong or does not existShow "invalid license key" and prompt re-entry
expiredKey was valid but the term lapsedLock features, prompt renewal
domain_not_activatedKey is fine but this site has no seatActivate the domain, then re-validate
revokedKey was cancelled (refund, chargeback, abuse)Lock features, do not retry endlessly

Next steps​

You now have a working gate. From here: