Cómo comprobar una clave de licencia dentro de tu propio producto: la biblioteca, la API, integraciones listas para once entornos y las reglas que mantienen en marcha las tiendas de tus compradores.
Una clave de licencia vincula tu producto a un dominio, o a tantos como el comprador haya pagado. Permite que un cliente honesto se mantenga dentro de lo que compró, te da el recuento de tus instalaciones y hace que una clave filtrada se pueda matar: su propietario la regenera y la filtrada muere. No es una protección contra la copia. Tu comprador tiene tu código fuente y puede quitar la comprobación en cinco minutos. Quien te diga lo contrario te está vendiendo algo.
El archivo que descargas se genera para un producto. La clase, el nombre del archivo y el archivo de caché llevan todos un nombre derivado de ese producto, y la dirección a la que llama se ensambla en tiempo de ejecución en vez de escribirse de una pieza. Dos de tus propios productos no comparten ni un solo identificador.
Seamos claros sobre lo que eso compra. Quien abra tus archivos y los lea encuentra la comprobación en dos minutos: eso no ha cambiado y no cambiará nunca. Lo que sí detiene es lo otro: un script que busca un nombre de clase conocido en miles de productos y lo quita solo. Así se fabrican las copias nulled en serie, y aquí ya no funciona. El coste de piratear tu producto pasa de nada a una revisión manual, producto por producto.
Los ejemplos de esta página llaman a la clase Licence. En tu descarga lleva su nombre real, mostrado junto al botón de descarga en tu área de autor.
Abre el que corresponda a tu producto. Cada bloque está completo y listo para pegar; renombra monmodule y adapta el almacenamiento de la clave a tus propios ajustes. Los entornos PHP usan la biblioteca descargable; los demás muestran el cliente completo, con caché y verificación de firma incluidas.
La forma más corta. Pon la comprobación en la entrada de aquello que proteges: un controlador frontal, una página de administración, un script programado.
require_once __DIR__ . '/Licence.php';
$licence = new Licence(
$config['licence'],
__DIR__ . '/cache'
);
if (!$licence->valide()) {
http_response_code(403);
exit($licence->message());
}
Mantén una sola instancia para todo el módulo. Comprueba en getContent() para que el comerciante vea por qué la pantalla de configuración se niega, y también en tus hooks de visualización para que un módulo a medio configurar no se muestre en la tienda.
require_once dirname(__FILE__) . '/Licence.php';
class MonModule extends Module
{
private $licence = null;
private function licence()
{
if ($this->licence === null) {
$this->licence = new Licence(
Configuration::get('MONMODULE_LICENCE'),
_PS_CACHE_DIR_ . 'monmodule'
);
}
return $this->licence;
}
public function getContent()
{
if (Tools::isSubmit('submitLicence')) {
Configuration::updateValue('MONMODULE_LICENCE', Tools::getValue('licence'));
}
$l = $this->licence();
if (!$l->valide()) {
return $this->displayError($l->message()) . $this->formulaireLicence();
}
if ($l->essai()) {
return $this->displayWarning($this->l('Development licence')) . $this->contenu();
}
return $this->contenu();
}
public function hookDisplayHeader()
{
if (!$this->licence()->valide()) {
return '';
}
return $this->contenuFront();
}
}
Comprobado en 1.7, 8 y 9. Las llamadas usadas — Module, Configuration, Tools, _PS_CACHE_DIR_ — han atravesado las eliminaciones de la 9.0 sin tocarse. Las deprecaciones anunciadas para la 10.0 afectan a los controladores de administración, que este ejemplo no usa.
Una instancia estática en una función, un aviso en la administración y el cuerpo del plugin detrás de la comprobación. No hagas la comprobación en el front de cada página: la caché la hace barata, pero un aviso pertenece a la administración.
require_once __DIR__ . '/Licence.php';
function monplug_licence() {
static $l = null;
if ($l === null) {
$l = new Licence(
get_option('monplug_licence'),
WP_CONTENT_DIR . '/cache/monplug'
);
}
return $l;
}
add_action('admin_notices', function () {
$l = monplug_licence();
if (!$l->valide()) {
printf('<div class="notice notice-error"><p>%s</p></div>', esc_html($l->message()));
} elseif ($l->essai()) {
echo '<div class="notice notice-warning"><p>'
. esc_html__('Development licence', 'monplug') . '</p></div>';
}
});
add_action('init', function () {
if (monplug_licence()->valide()) {
monplug_demarrer();
}
});
Las cinco llamadas usadas — add_action, add_filter, get_option, esc_html, WP_CONTENT_DIR — no han cambiado desde WordPress 5.0 y siguen funcionando en la rama 7.1. Nada de esto depende del editor de bloques ni de una ruta REST.
En WooCommerce, condiciona lo que añades a la tienda en lugar del plugin entero: una pasarela de pago que desaparece es más clara para el comerciante que un plugin que muere en silencio.
add_filter('woocommerce_payment_gateways', function ($passerelles) {
if (monplug_licence()->valide()) {
$passerelles[] = 'WC_Gateway_MonPlug';
}
return $passerelles;
});
add_action('woocommerce_admin_field_monplug_licence', function () {
$l = monplug_licence();
if (!$l->valide()) {
echo '<div class="error inline"><p>' . esc_html($l->message()) . '</p></div>';
}
});
Envuelve la biblioteca en un modelo e inyéctalo donde lo necesites. El directorio de caché debe ser el de Magento, o un despliegue borrará tu veredicto en cada publicación.
namespace Editeur\MonModule\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
class Licence
{
private $licence;
public function __construct(ScopeConfigInterface $config, DirectoryList $dossiers)
{
$this->licence = new \Licence(
(string) $config->getValue('monmodule/general/licence'),
$dossiers->getPath(DirectoryList::CACHE) . '/monmodule'
);
}
public function valide(): bool
{
return $this->licence->valide();
}
public function message(): string
{
return $this->licence->message();
}
}
La biblioteca no tiene espacio de nombres: decláralas en el composer.json de tu módulo en lugar de llamar a require_once a mano.
{
"autoload": {
"files": [
"Licence.php"
]
}
}
Un middleware es el sitio adecuado: regístralo en el grupo de rutas que pertenece a tu producto, nunca de forma global, para que un problema de licencia nunca tumbe el resto de la aplicación.
namespace App\Licence;
use Closure;
use Illuminate\Http\Request;
class VerifieLicence
{
private $licence;
public function __construct()
{
$this->licence = new \Licence(
config('monmodule.licence'),
storage_path('app/monmodule')
);
}
public function handle(Request $requete, Closure $suivant)
{
if (!$this->licence->valide()) {
abort(403, $this->licence->message());
}
return $suivant($requete);
}
}
Un suscriptor de eventos en kernel.request, que ignora las subpeticiones. Pasa la clave y el directorio de caché como argumentos en tu definición de servicio en lugar de leer la configuración dentro de la clase.
namespace App\Licence;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
class LicenceSubscriber implements EventSubscriberInterface
{
private $licence;
public function __construct(string $cle, string $dossierCache)
{
$this->licence = new \Licence($cle, $dossierCache);
}
public function onKernelRequest(RequestEvent $evenement): void
{
if (!$evenement->isMainRequest()) {
return;
}
if (!$this->licence->valide()) {
throw new AccessDeniedHttpException($this->licence->message());
}
}
public static function getSubscribedEvents(): array
{
return [KernelEvents::REQUEST => ['onKernelRequest', 16]];
}
}
isMainRequest() llegó en Symfony 5.3. En una versión anterior, llama a isMasterRequest() en su lugar: todo lo demás es idéntico.
Ninguna dependencia más allá de la biblioteca estándar. Este es el cliente completo: respuesta firmada, caché vinculada a su host, reutilización de 24 horas y prórroga de 30 días, las mismas reglas que la biblioteca PHP.
import { createPublicKey, verify } from 'node:crypto';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
const API = 'https://addonsmarket.com/api/licence';
const PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=';
const SURSIS = 2592000;
const publique = createPublicKey({
key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'),
Buffer.from(PUBLIQUE, 'base64')]),
format: 'der',
type: 'spki',
});
const canonique = (d) =>
JSON.stringify(Object.fromEntries(Object.keys(d).sort().map((k) => [k, d[k]])));
const signee = (rep, hote) =>
rep && rep.donnees && rep.signature
&& rep.donnees.hote === hote
&& verify(null, Buffer.from(canonique(rep.donnees), 'utf8'), publique,
Buffer.from(rep.signature, 'base64'));
export async function etat(cle, hote, fichierCache) {
const maintenant = Math.floor(Date.now() / 1000);
let cache = null;
try {
const lu = JSON.parse(await readFile(fichierCache, 'utf8'));
if (signee(lu, hote)) cache = lu;
} catch {}
if (cache && cache.donnees.revalider > maintenant) return cache.donnees;
try {
const r = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'User-Agent': 'MonProduit/1.0' },
body: JSON.stringify({ cle, url: hote }),
signal: AbortSignal.timeout(6000),
});
if (!r.ok) throw new Error(String(r.status));
const rep = await r.json();
if (!signee(rep, hote)) throw new Error('signature');
await mkdir(dirname(fichierCache), { recursive: true });
await writeFile(fichierCache, JSON.stringify(rep));
return rep.donnees;
} catch {
if (cache && cache.donnees.emis + SURSIS > maintenant) return cache.donnees;
return { ok: false, motif: 'reseau' };
}
}
Node 18 o posterior: fetch pasó a ser global en la 18, y AbortSignal.timeout en la 17.3. En un runtime anterior, sustituye esos dos por https.request y tu propio temporizador.
Después llámalo una vez por petición, y nunca bloquees por un fallo de red.
import { etat } from './licence.mjs';
const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');
if (!d.ok && d.motif !== 'reseau') {
return res.status(403).json({ erreur: d.motif });
}
No hay forma honesta de comprobar una licencia en un navegador. Lo que escribas se ejecuta en la máquina del comprador, en código que puede leer, y quitarlo cuesta una línea en las herramientas de desarrollo. No ofrecemos una biblioteca de front porque sería venderte un placebo.
Si tu producto tiene servidor, comprueba allí y expón el veredicto como un dato. La interfaz podrá entonces decir algo útil sin que la comprobación esté en el navegador.
app.get('/api/etat', async (req, res) => {
const d = await etat(process.env.LICENCE, req.hostname, './var/licence.json');
res.json({ actif: d.ok, essai: d.essai });
});
const [actif, setActif] = useState(null);
useEffect(() => {
fetch('/api/etat')
.then((r) => r.json())
.then((d) => setActif(d.actif));
}, []);
if (actif === false) {
return <Bandeau>Licence inactive</Bandeau>;
}
Si tu producto es una plantilla de front sin ningún servidor, no hay nada que comprobar en tiempo de ejecución y no conviene fingir lo contrario. La licencia se aplica donde puede: en la descarga y mediante los términos que tu comprador aceptó.
Una app de Shopify tiene su propio servidor: usa allí el cliente Node o PHP y envía el dominio de la tienda, la dirección myshopify.com o el dominio propio si es en él en el que te apoyas. Un tema de Shopify no tiene servidor, así que se aplica la sección de front de arriba.
Biblioteca estándar más cryptography para la firma. Las mismas reglas que en todas partes: reutilizar 24 horas, conservar el último veredicto 30 días si callamos, y bloquear solo ante un rechazo firmado.
import base64, json, time, urllib.request
from pathlib import Path
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
API = 'https://addonsmarket.com/api/licence'
PUBLIQUE = '/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw='
SURSIS = 2592000
_publique = Ed25519PublicKey.from_public_bytes(base64.b64decode(PUBLIQUE))
def _canonique(d):
return json.dumps(d, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
def _signee(rep, hote):
try:
if rep['donnees']['hote'] != hote:
return False
_publique.verify(base64.b64decode(rep['signature']),
_canonique(rep['donnees']).encode('utf-8'))
return True
except (KeyError, TypeError, ValueError, InvalidSignature):
return False
def etat(cle, hote, fichier_cache):
maintenant = int(time.time())
chemin = Path(fichier_cache)
cache = None
try:
lu = json.loads(chemin.read_text(encoding='utf-8'))
if _signee(lu, hote):
cache = lu
except (OSError, ValueError):
pass
if cache and cache['donnees']['revalider'] > maintenant:
return cache['donnees']
try:
corps = json.dumps({'cle': cle, 'url': hote}).encode('utf-8')
requete = urllib.request.Request(API, data=corps, headers={
'Content-Type': 'application/json',
'User-Agent': 'MonProduit/1.0',
})
with urllib.request.urlopen(requete, timeout=6) as r:
rep = json.loads(r.read().decode('utf-8'))
if not _signee(rep, hote):
raise ValueError('signature')
chemin.parent.mkdir(parents=True, exist_ok=True)
chemin.write_text(json.dumps(rep), encoding='utf-8')
return rep['donnees']
except Exception:
if cache and cache['donnees']['emis'] + SURSIS > maintenant:
return cache['donnees']
return {'ok': False, 'motif': 'reseau'}
Para todo lo demás: Go, Ruby, .NET, Java, un script de shell. Un POST, un cuerpo JSON. Los campos están en la sección de la API más abajo, y lo que debes verificar antes de fiarte de la respuesta, en la sección de la firma.
curl -X POST https://addonsmarket.com/api/licence \
-H 'Content-Type: application/json' \
-H 'User-Agent: MonProduit/1.0' \
-d '{"cle":"VOTRE_CLE","url":"boutique.exemple.fr"}'
Para un tema o un script vendido a alguien que no es desarrollador, tumbar el sitio ante una copia sin licencia es el gesto equivocado: quien lo sufre suele ser el comprador que copió mal su clave, y lo paga tu bandeja de soporte. banniere() devuelve en su lugar un pequeño aviso fijo: nada más cambia en la página.
require_once __DIR__ . '/Licence.php';
$reglages = @include __DIR__ . '/licence.php';
$licence = new Licence(
is_array($reglages) ? $reglages['cle'] : '',
__DIR__ . '/cache'
);
register_shutdown_function(function () use ($licence) {
echo $licence->banniere('My theme');
});
El comprador lo quita poniendo su clave en un archivo, junto a la biblioteca. Ese archivo es lo único que tiene que tocar.
<?php
return array(
'cle' => '',
);
Devuelve una cadena vacía cuando la licencia es válida, y también cuando la red ha fallado: un visitante nunca ve un aviso porque nuestro servidor pasara una mala noche. Con una clave de desarrollo lo dice, con discreción, que es justo lo que quieres mientras construyes.
Un tema HTML, un archivo de Figma, un juego de iconos, una tipografía: nada de eso se ejecuta en un servidor al que puedas preguntar. Todo lo que entregas lo lee, lo modifica y lo redistribuye quien tenga los archivos. No hay comprobación que añadir, y no existe ninguna versión de esta página donde eso cambie.
No finjas. Un script ofuscado que llama a casa desde una página estática, una huella oculta en un canvas, una compilación que se rompe a los treinta días: todo eso lo quita en una tarde la única persona que iba a piratearte, y todo eso acaba en un ticket de soporte de los cincuenta que pagaron. Estarías gastando tu credibilidad para no proteger nada.
Lo que existe en su lugar actúa en la entrega, no en la ejecución. Cada descarga se reempaqueta para el comprador que la pidió, y lleva tres cosas:
Eso es trazabilidad, no prevención, y no te la vamos a vender como otra cosa: un comprador que borre el archivo y vacíe el comentario se va con una copia limpia. Lo que sí te da es un nombre cuando una copia aparece donde no debería, un motivo para que ese comprador se lo piense dos veces, y las dos cosas que una copia pirata nunca tiene: las actualizaciones y tu soporte.
Aquí importa una distinción. Si tu tema tiene algo de PHP — un formulario de contacto, un archivo de configuración, un include — entonces se ejecuta en un servidor, la comprobación de arriba es real y el aviso funciona. Solo en el caso totalmente estático, HTML y CSS y nada más, no hay comprobación posible en ejecución.
Un solo archivo PHP, sin dependencias, compatible hasta PHP 5.6 para que funcione en las instalaciones antiguas que aún tienen tus compradores. Bastan cuatro métodos.
| new Licence($cle, $dossierCache) | Constrúyela con la clave del comprador y un directorio con permiso de escritura para la caché. |
| $licence->valide() | Verdadero cuando el producto puede funcionar en este dominio. |
| $licence->essai() | Verdadero con una clave de desarrollo o un dominio de pruebas. Muestra un aviso discreto: nunca publiques una versión que lo oculte. |
| $licence->message() | Una frase para mostrar al comprador, ya traducida, que nombra los dominios y fecha el suceso. |
Nunca dejes que un problema de red tumbe la tienda de tu comprador. La biblioteca ya se ocupa de ello y no debes esquivarla: un veredicto reciente se reutiliza 24 horas; cuando nuestro servidor no responde, el último veredicto conocido se conserva 30 días y el producto sigue funcionando; solo un rechazo firmado lo detiene, de inmediato. Si nuestro servidor pasa una mala noche, ninguna tienda se apaga. Si una clave se transfiere, el sitio antiguo se detiene en menos de 24 horas.
Una licencia se vincula al dominio registrable. Una clave en ejemplo.es cubre www.ejemplo.es, tienda.ejemplo.es y cualquier otro subdominio: tu comprador no te escribirá por una redirección que falta. Las direcciones de prueba nunca gastan licencia: localhost, .local, .test y los prefijos habituales dev., staging., preprod. funcionan sin consumir nada.
La biblioteca no es más que una envoltura fina sobre un único punto de entrada. Llámalo directamente si trabajas fuera de PHP, pero entonces tendrás que implementar tú mismo las reglas de caché anteriores y verificar la firma.
POST https://addonsmarket.com/api/licence
POST /api/licence HTTP/1.1
Host: addonsmarket.com
Content-Type: application/json
User-Agent: MonProduit/1.0
{
"cle": "VOTRE_CLE",
"url": "boutique.exemple.fr"
}
| cle | string | La clave del comprador. Solo letras y dígitos; quitamos lo demás, así que los espacios y guiones pegados no molestan. |
| url | string | El host en el que funciona tu producto. Una URL completa también sirve: solo nos quedamos con el host. Quita tú mismo el puerto y el www. inicial si quieres que la respuesta coincida con lo que enviaste. |
{
"donnees": {
"ok": true,
"motif": "",
"domaine": "exemple.fr",
"essai": false,
"quand": "",
"emis": 1790687453,
"revalider": 1790773853,
"sursis": 2592000,
"hote": "boutique.exemple.fr"
},
"signature": "4p9JXJRfcB5R/LNEQwr/cwnVv1M5R1OdCNcU…"
}
| ok | bool | Si el producto puede funcionar aquí. |
| motif | string | Por qué no, cuando ok es falso. Mira la tabla de abajo. |
| domaine | string | El dominio al que está vinculada la licencia. |
| essai | bool | Clave de desarrollo o dominio de pruebas. |
| quand | string | Cuándo se reemplazó o se revocó la clave. |
| emis | int | Cuándo respondimos. El comienzo de la prórroga de 30 días. |
| revalider | int | Marca de tiempo antes de la cual no debes volver a preguntar. |
| sursis | int | Segundos durante los que puedes conservar este veredicto si dejamos de responder. |
| hote | string | El host sobre el que hemos respondido. Rechaza la respuesta si no es el tuyo. |
| 200 | Un veredicto firmado. Ojo: un rechazo también es un 200; lee ok, no el código de estado. |
| 400 | Falta la clave o el host. |
| 405 | Cualquier cosa que no sea POST u OPTIONS. |
| 429 | Límite de llamadas alcanzado. Conserva tu veredicto en caché y reinténtalo más tarde; nunca lo trates como un rechazo. |
Treinta llamadas por minuto y por dirección IP. Si se respetan las reglas de caché, una tienda hace una llamada al día, así que solo alcanzarás este límite desde una prueba de carga o un bucle que olvidó su caché.
Envía un User-Agent que nombre tu producto. Algunos agentes por defecto de bibliotecas HTTP, como el de urllib en Python, se filtran antes de llegarnos y reciben un 403. Cualquier cadena personalizada lo evita.
Sin firma, una línea en el archivo hosts de un servidor basta para responder «licencia válida» en nuestro lugar. Cada respuesta lleva una firma Ed25519 sobre la forma canónica de los datos: claves ordenadas, JSON compacto. Verifícala antes de fiarte de nada. La biblioteca lo hace por ti, incluso sobre la copia en caché, que a su vez está vinculada a su host para que no pueda copiarse de un sitio a otro.
Solo hace falta si llamas a la API directamente. Reconstruye la forma canónica a partir del objeto analizado, nunca de los bytes en crudo recibidos, que pueden diferir, y después verifica la firma Ed25519 separada con nuestra clave pública.
Ed25519, 32 bytes en crudo en base64. Es la misma para todos los autores y todos los productos.
/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=
La firma cubre el objeto donnees serializado con estas cuatro reglas, y nada más. Falla una y todas las firmas parecerán inválidas.
Para la respuesta de arriba, los bytes firmados exactamente:
{"domaine":"exemple.fr","emis":1790687453,"essai":false,"hote":"boutique.exemple.fr","motif":"","ok":true,"quand":"","revalider":1790773853,"sursis":2592000}
function signature_valide(array $donnees, $signature, $publique)
{
ksort($donnees);
$corps = json_encode($donnees, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return sodium_crypto_sign_verify_detached(
base64_decode($signature, true),
$corps,
base64_decode($publique, true)
);
}
import { createPublicKey, verify } from 'node:crypto';
const publique = createPublicKey({
key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'),
Buffer.from('/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw=', 'base64')]),
format: 'der',
type: 'spki',
});
const canonique = (d) =>
JSON.stringify(Object.fromEntries(Object.keys(d).sort().map((k) => [k, d[k]])));
const valide = verify(null, Buffer.from(canonique(rep.donnees), 'utf8'),
publique, Buffer.from(rep.signature, 'base64'));
import base64, json
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
publique = Ed25519PublicKey.from_public_bytes(base64.b64decode('/PTkKQpweo9XCLOS0+b0vgrecx7Ly/SsAuwTiA737Tw='))
canonique = json.dumps(rep['donnees'], sort_keys=True,
separators=(',', ':'), ensure_ascii=False)
publique.verify(base64.b64decode(rep['signature']), canonique.encode('utf-8'))
| autre_domaine | La clave es válida pero pertenece a otro dominio. Dile al comprador que la transfiera desde su cuenta. |
| transfert | La clave se ha movido a otro sitio y se ha reemplazado. El comprador tiene una nueva. |
| regeneration | El comprador pidió una clave nueva. La antigua está muerta para siempre. |
| revocation | Hemos revocado la clave: abuso, fraude o una disputa resuelta. |
| expiree | Una clave de prueba con plazo que ha caducado. |
| inconnue | Esa clave no existe. Normalmente una errata. |
| reseau | No se nos ha podido contactar y no queda ningún veredicto en caché. No bloquees por esto. |
Tu área de autor te da una clave de desarrollo para cada uno de tus productos. Funciona en cualquier dominio, solo abre tus propios productos y siempre informa essai: así, una instalación que la use muestra un aviso de desarrollo y nunca puede confundirse con una copia vendida.
Para ver cómo se comporta tu producto cuando se le rechaza, envía una clave que no exista. Obtienes un rechazo firmado real, idéntico al de una clave revocada, así que puedes comprobar tu pantalla de error sin revocar nada.
curl -X POST https://addonsmarket.com/api/licence \
-H 'Content-Type: application/json' \
-H 'User-Agent: MonProduit/1.0' \
-d '{"cle":"cettecleNexistePas000000","url":"boutique.exemple.fr"}'
{"donnees":{"ok":false,"motif":"inconnue", …},"signature":"…"}
Tu producto enviará a nuestro servidor el dominio en el que funciona. Eso es un tratamiento de datos personales según el derecho europeo, y tu comprador debe saberlo. Dilo en la descripción de tu producto y en tu propia documentación. Conservamos el dominio, y la IP durante doce meses para resolver disputas; nada más, y no vendemos nada de ello.
Escríbenos desde tu área de autor. Una pregunta que hubo que hacer suele significar que a esta página le falta un párrafo.