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: