Skip to main content

License Verification Guide

This guide explains how to implement ECOSIRE license checking in your own application — whether you are building an Odoo module, a SaaS product, or any software that should be gated behind a purchased license.


Introduction

The ECOSIRE licensing system uses:

  • License keys in the format PREFIX-XXXX-XXXX-XXXX-XXXX (e.g., SHOP-A1B2-C3D4-E5F6-G7H8)
  • Domain binding — each activation ties a license to a specific domain/installation
  • HMAC signing — requests are signed with SHA256 to prevent forgery
  • Activation limits — a license allows a configurable number of concurrent activations

Prerequisites

  • ECOSIRE account with licenses issued (via purchase or admin grant)
  • ECOSIRE_LICENSE_SECRET from your ECOSIRE admin settings
  • Python 3.9+ or any language that supports HMAC-SHA256

Step 1 — Understand the License Lifecycle

License issued (on purchase or admin grant)

Customer enters license key in your app

App calls POST /licenses/validate ← check if key is valid

App calls POST /licenses/activate ← bind domain to license

App periodically re-validates (weekly heartbeat)

License expires or is revoked → app disables features

Step 2 — Validate a License Key

Before allowing use, check if the key is valid:

import hmac, hashlib, time, requests, os

LICENSE_SECRET = os.environ.get("ECOSIRE_LICENSE_SECRET", "")
API_BASE = "https://api.ecosire.com/api"

def build_signature(payload: dict) -> str:
"""HMAC-SHA256 sign the payload sorted keys as query string."""
message = "&".join(f"{k}={v}" for k, v in sorted(payload.items()))
return hmac.new(
LICENSE_SECRET.encode(),
message.encode(),
hashlib.sha256
).hexdigest()

def validate_license(key: str, domain: str) -> dict:
payload = {
"key": key,
"domain": domain,
"timestamp": str(int(time.time())),
}
signature = build_signature(payload)

resp = requests.post(
f"{API_BASE}/licenses/validate",
json=payload,
headers={"X-Ecosire-Signature": signature},
timeout=10,
)
return resp.json()

result = validate_license("SHOP-A1B2-C3D4-E5F6-G7H8", "mystore.myshopify.com")
if result.get("valid"):
print("License valid! Status:", result["license"]["status"])
else:
print("Invalid license:", result.get("message"))

Step 3 — Activate a License

Activation binds the license to a specific domain or installation fingerprint. Call this once when the user first installs your software.

def activate_license(key: str, domain: str) -> dict:
payload = {
"key": key,
"domain": domain,
"timestamp": str(int(time.time())),
}
signature = build_signature(payload)

resp = requests.post(
f"{API_BASE}/licenses/activate",
json=payload,
headers={"X-Ecosire-Signature": signature},
timeout=10,
)
return resp.json()

result = activate_license("SHOP-A1B2-C3D4-E5F6-G7H8", "mystore.myshopify.com")
if result.get("activated"):
print(f"Activated! Slots used: {result['activationsCount']}/{result['activationsLimit']}")
else:
print("Activation failed:", result.get("message"))

Step 4 — Implement in an Odoo Module

The ECOSIRE Odoo client module (ecosire_license_client) handles this automatically. For custom modules, add a license check to your __init__.py:

# your_module/__init__.py
import requests, logging, odoo
from odoo.exceptions import ValidationError

_logger = logging.getLogger(__name__)

def check_license():
try:
config = odoo.tools.config
license_key = config.get("ecosire_license_key", "")
site_url = config.get("web.base.url", "")

if not license_key:
_logger.warning("ECOSIRE license key not configured.")
return

resp = requests.post(
"https://api.ecosire.com/api/licenses/validate",
json={"key": license_key, "domain": site_url},
timeout=5,
)
data = resp.json()
if not data.get("valid"):
raise ValidationError(f"License invalid: {data.get('message', 'Unknown error')}")

_logger.info("ECOSIRE license validated successfully.")

except requests.exceptions.RequestException:
_logger.warning("Could not reach ECOSIRE license server — using grace period.")

check_license()

Add the license key to odoo.conf:

ecosire_license_key = SHOP-A1B2-C3D4-E5F6-G7H8

Step 5 — Implement a Heartbeat Check

Re-validate weekly to catch revoked or expired licenses:

import threading, time

def license_heartbeat(key: str, domain: str, interval_seconds: int = 604800):
"""Re-validate every 7 days."""
while True:
time.sleep(interval_seconds)
result = validate_license(key, domain)
if not result.get("valid"):
# Disable premium features
disable_premium_features()
break

thread = threading.Thread(target=license_heartbeat, args=("SHOP-A1B2-...", "mystore.myshopify.com"), daemon=True)
thread.start()

Step 6 — Offline Grace Period

When the ECOSIRE API is unreachable, allow a grace period (7–30 days) before disabling features:

import json
from pathlib import Path
from datetime import datetime, timedelta

CACHE_FILE = Path("/tmp/ecosire_license_cache.json")
GRACE_DAYS = 7

def validate_with_cache(key: str, domain: str) -> bool:
try:
result = validate_license(key, domain)
if result.get("valid"):
CACHE_FILE.write_text(json.dumps({
"valid": True,
"cachedAt": datetime.utcnow().isoformat(),
}))
return True
return False
except Exception:
# Offline — check cache
if CACHE_FILE.exists():
cache = json.loads(CACHE_FILE.read_text())
cached_at = datetime.fromisoformat(cache["cachedAt"])
if datetime.utcnow() - cached_at < timedelta(days=GRACE_DAYS):
return cache.get("valid", False)
return False

Step 7 — Deactivate on Uninstall

When the user removes your software, release the activation slot:

def deactivate_license(key: str, domain: str):
payload = {"key": key, "domain": domain, "timestamp": str(int(time.time()))}
signature = build_signature(payload)
requests.post(
f"{API_BASE}/licenses/deactivate",
json=payload,
headers={"X-Ecosire-Signature": signature},
timeout=10,
)

Troubleshooting

ErrorCauseSolution
License not foundIncorrect key formatVerify format: PREFIX-XXXX-XXXX-XXXX-XXXX
Activation limit reachedAll slots usedUser must deactivate another domain first
Invalid signatureLICENSE_SECRET mismatchCopy secret from ECOSIRE admin settings
License expiredPast expiryCustomer should renew from their dashboard
License suspendedSubscription past dueCustomer must update payment method

Next Steps