Verify API Reference
Use this endpoint inside the products you sell to confirm that a customer's license key is valid and permitted to run in the current environment.
Endpoint
POST /api/v1/licenses/verifyFull URL example:
https://your-store.example.com/api/v1/licenses/verifyNo authentication is required — the endpoint is intentionally public so your customers' installed software can call it without needing a user account or API token.
Request
Headers
Content-Type: application/json
Accept: application/jsonBody
{
"license_key": "CP-AB12CD-34EF56-GH78IJ-90KLMN-XYZ123",
"domain": "customer-site.example.com",
"ip": "203.0.113.42"
}| Field | Type | Required | Description |
|---|---|---|---|
license_key | string | Yes | The full license key as issued to the customer. |
domain | string | Yes | The domain where your product is installed, e.g. example.com. |
ip | string (valid IP) | Yes | A syntactically valid IP address. The server uses the actual connection IP for its checks — the value you send here is validated for format only. Pass the server's real IP or any valid placeholder. |
Responses
Success — HTTP 200
{
"valid": true,
"product": "Acme Theme Pro",
"expiry": "2026-12-31",
"lifetime": false
}| Field | Type | Description |
|---|---|---|
valid | boolean | Always true on a 200 response. |
product | string | The product name stored on the license at the time it was issued. |
expiry | string | null | The expiry date in YYYY-MM-DD format, or null if the key has no expiry. |
lifetime | boolean | true when the key never expires (expiry will be null). |
Failure — HTTP 403 or 404
{
"valid": false,
"reason": "License has been revoked."
}| HTTP | reason | When this happens |
|---|---|---|
| 403 | Invalid license format. | The key doesn't match the CP-... key shape. |
| 404 | License not found. | No key with that value exists in the store. |
| 403 | License has been revoked. | The store owner or a refund has revoked this key. |
| 403 | License expired. | The key has passed its expiry date, or the subscription it was tied to has ended. |
| 403 | Max activations exceeded or unrecognized environment. | The domain/IP isn't in the key's allow-list and all activation slots are filled. |
Validation error — HTTP 422
If any required field is missing or the ip value isn't a valid IP address, Laravel returns a standard 422 validation response:
{
"message": "The given data was invalid.",
"errors": {
"ip": ["The ip must be a valid IP address."]
}
}Code examples
curl
curl -X POST https://your-store.example.com/api/v1/licenses/verify \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"license_key": "CP-AB12CD-34EF56-GH78IJ-90KLMN-XYZ123",
"domain": "customer-site.example.com",
"ip": "203.0.113.42"
}'PHP (Guzzle / Laravel Http)
use Illuminate\Support\Facades\Http;
$response = Http::post('https://your-store.example.com/api/v1/licenses/verify', [
'license_key' => $licenseKey,
'domain' => request()->getHost(),
'ip' => request()->ip(),
]);
if ($response->successful() && $response->json('valid')) {
// License is valid — activate the product
$product = $response->json('product');
$expiry = $response->json('expiry'); // null if lifetime
} else {
$reason = $response->json('reason', 'Verification failed.');
// Handle failure: show $reason to the customer
}JavaScript (fetch)
const res = await fetch('https://your-store.example.com/api/v1/licenses/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
license_key: licenseKey,
domain: window.location.hostname,
ip: '0.0.0.0', // server will use the real connection IP; any valid IP passes validation
}),
});
const data = await res.json();
if (data.valid) {
console.log('Valid license for:', data.product);
} else {
console.warn('Verification failed:', data.reason);
}Node.js (axios)
import axios from 'axios';
try {
const { data } = await axios.post(
'https://your-store.example.com/api/v1/licenses/verify',
{
license_key: licenseKey,
domain: 'customer-site.example.com',
ip: '0.0.0.0',
},
{ headers: { Accept: 'application/json' } }
);
if (data.valid) {
console.log('Valid license for:', data.product);
console.log('Expires:', data.lifetime ? 'Never' : data.expiry);
}
} catch (err) {
const reason = err.response?.data?.reason ?? 'Verification failed.';
console.error('License error:', reason);
}Python (requests)
import requests
import socket
payload = {
"license_key": license_key,
"domain": "customer-site.example.com",
"ip": socket.gethostbyname(socket.gethostname()), # or pass a known IP
}
response = requests.post(
"https://your-store.example.com/api/v1/licenses/verify",
json=payload,
headers={"Accept": "application/json"},
timeout=10,
)
data = response.json()
if response.ok and data.get("valid"):
print("Valid license for:", data["product"])
print("Expires:", "Never" if data["lifetime"] else data["expiry"])
else:
print("Verification failed:", data.get("reason", "Unknown error"))Ruby (Net::HTTP)
require 'net/http'
require 'json'
require 'uri'
uri = URI('https://your-store.example.com/api/v1/licenses/verify')
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request['Accept'] = 'application/json'
request.body = {
license_key: license_key,
domain: 'customer-site.example.com',
ip: '203.0.113.42'
}.to_json
http.request(request)
end
body = JSON.parse(response.body)
if response.is_a?(Net::HTTPSuccess) && body['valid']
puts "Valid license for: #{body['product']}"
puts "Expires: #{body['lifetime'] ? 'Never' : body['expiry']}"
else
puts "Verification failed: #{body['reason']}"
endGo
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type verifyRequest struct {
LicenseKey string `json:"license_key"`
Domain string `json:"domain"`
IP string `json:"ip"`
}
type verifyResponse struct {
Valid bool `json:"valid"`
Product string `json:"product"`
Expiry *string `json:"expiry"`
Lifetime bool `json:"lifetime"`
Reason string `json:"reason"`
}
func verifyLicense(licenseKey, domain, ip string) (*verifyResponse, error) {
body, _ := json.Marshal(verifyRequest{LicenseKey: licenseKey, Domain: domain, IP: ip})
resp, err := http.Post(
"https://your-store.example.com/api/v1/licenses/verify",
"application/json",
bytes.NewBuffer(body),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result verifyResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}
func main() {
result, err := verifyLicense("CP-AB12CD-34EF56-GH78IJ-90KLMN-XYZ123", "customer-site.example.com", "203.0.113.42")
if err != nil {
fmt.Println("Request error:", err)
return
}
if result.Valid {
fmt.Println("Valid license for:", result.Product)
} else {
fmt.Println("Verification failed:", result.Reason)
}
}C# (HttpClient)
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public record VerifyRequest(
[property: JsonPropertyName("license_key")] string LicenseKey,
[property: JsonPropertyName("domain")] string Domain,
[property: JsonPropertyName("ip")] string Ip
);
public record VerifyResponse(
[property: JsonPropertyName("valid")] bool Valid,
[property: JsonPropertyName("product")] string? Product,
[property: JsonPropertyName("expiry")] string? Expiry,
[property: JsonPropertyName("lifetime")] bool Lifetime,
[property: JsonPropertyName("reason")] string? Reason
);
// --- Usage ---
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
var payload = new VerifyRequest(licenseKey, "customer-site.example.com", "203.0.113.42");
var httpResponse = await client.PostAsJsonAsync(
"https://your-store.example.com/api/v1/licenses/verify",
payload
);
var result = await httpResponse.Content.ReadFromJsonAsync<VerifyResponse>();
if (result?.Valid == true)
{
Console.WriteLine($"Valid license for: {result.Product}");
Console.WriteLine($"Expires: {(result.Lifetime ? "Never" : result.Expiry)}");
}
else
{
Console.WriteLine($"Verification failed: {result?.Reason}");
}Verifying that a key belongs to a specific product
The endpoint validates the key itself — its format, existence, revocation status, expiry, and whether the calling environment is allowed. It does not enforce that a key was issued for a particular product.
If your integration needs to confirm a key belongs to the product you expect, compare the product field in the success response against the name you assigned to your product in the store:
$response = Http::post(..., [...]);
if ($response->json('valid') && $response->json('product') === 'Acme Theme Pro') {
// Key is valid AND issued for this specific product
}The product value is a name snapshot taken at the time the key was issued, so it will match the product name as it appeared in your store at the time of purchase.
Caching recommendations
Each call from a new domain or IP address consumes one activation slot. To avoid depleting a customer's activation allowance or adding unnecessary load to your store:
- Cache a successful verification response locally (e.g. in a transient, file cache, or database) for at least 24 hours.
- Only re-verify when the cached result expires or when a customer explicitly triggers a re-check.
- Do not call the endpoint on every page load.