# Licensing a Marketplace Product

https://dev.wisecp.com/es/marketplace-product-licensing

Tell the marketplace how to ask your own server whether an installation holds your product.

## Overview

WISECP publishes no licence protocol for the products you sell. You choose the address, what travels with the question, and what a yes looks like.

Entitlement is not the panel's to assert, so it asks you, about one thing: the domain of the installation that is downloading.

Declaring a check is optional. A paid listing without one publishes, and its download stays open at the same bar as a free product.

## Prerequisites

- **A commercial listing**: Appears the moment the pricing model asks for money. A free product has nothing to license.
- **A reachable https address**: Public and answering directly. Private addresses are refused, and a redirect is never followed.
- **An answer that turns on a domain**: The question is always about one domain. A server that cannot decide from one cannot be wired to this.
- **A domain you know is licensed**: The test needs a real yes: an address that refuses everything is still reachable.

## Structure

Turn on **Declare a licence check**, then pick which kind. The two are not interchangeable settings; they are two different integrations, and each asks for its own fields.

| Mode | Who it fits | What you supply |
| --- | --- | --- |
| WISECP | You run WISECP yourself and sell the product as software with a licence key | The check address your own build already embeds |
| Custom | Anything else: your own API, a licence service, a static allow list | The request and what a positive answer looks like |

The panel asks at two moments, never on a page view: a buyer starting a download, and a customer beginning a review. A positive answer is remembered for ten minutes.

## Walkthrough

### Wire the WISECP mode

1. Open your software product in your own panel and go to its **Licensing** tab. The check address is shown there, ready to copy: `https://your-site.example/license/checking/{token}/{product-id}`.
2. Paste it into **Check address**. Nothing else is asked: the request and the answer are both known.
3. The panel calls it with the buyer's domain. Your installation answers with the plain word `OK`, or with the newer signed envelope. Both are read as a yes.

> **Copy the address, do not type it**
> 
> That address is the controller path, not the translated route visitors see. The route name changes with the language; this one does not.

### Wire a custom endpoint

1. Enter the address and choose **POST** or **GET**.
2. Name the field the domain travels in. Leave it empty and `domain` is used.
3. Add any constant fields your endpoint needs, one `name: value` per line, and the same for request headers.
4. Describe a yes. Give the status you expect, the body type, the path into it, and the value that means licensed.

### Test it

1. Enter a domain that really holds a licence on your server.
2. Click **Test connection**. The call is made from the panel, not your browser, so your credentials never travel to the page.
3. A pass stamps the declaration and completes the section. Editing any part of the declaration drops that stamp, so test again after a change.

### Check the licence inside your product

The declaration above is the download gate. It says nothing about the copy already installed, so the product enforces itself.

1. Ask your own server from inside your code. `License::remote_check()` sends the question and remembers the answer for a day, so a licensed product does not phone home on every page view.
2. Read the verdict yourself. It does not judge the answer or verify a signature; `null` means the server could not be reached, not that the copy is unlicensed.
3. Put the decision in a file you marked for encryption, so the line cannot be deleted. Marking and packaging: [Licensing a Module](https://dev.wisecp.com/en/licensing-a-module).

## Reference

The fields of a custom declaration.

- **Address**: Required. Redirects are not followed, so paste the final address.
- **Method**: POST or GET. A GET carries the fields as a query string.
- **Domain field**: The name the domain arrives under. Defaults to `domain`.
- **Extra fields**: Optional constants sent on every call, one `name: value` per line.
- **Headers**: Optional request headers, same form. Keep any token you send narrow.
- **Expected status**: A mismatch is a refusal. Use 0 when your endpoint answers 200 either way.
- **Body type**: JSON or plain text. Plain text judges the body itself and asks for no path.
- **Path**: A dot separated route into the JSON, such as `data.valid`. JSON with no path is a refusal.
- **Expected value**: Compared without case. Leave it empty to accept any truthy value; `0`, `false`, `null`, `no` and an empty value are refusals.

What a failed check reports back to you.

| Reason | What it means |
| --- | --- |
| Unreachable | No answer arrived. A declared check that cannot be reached refuses. |
| Status | Your server answered, with a status other than the expected one. |
| Refused | Your server answered, and the answer did not match what you described. |
| Bad address | WISECP mode. The token belongs to another product, or the address carries no token. |
| Bad request | WISECP mode. Your endpoint says required fields did not arrive, so something in front of it dropped them. |

## Example

A JSON endpoint that answers 200 with a nested verdict.

```json
{"data": {"valid": true, "expires_at": "2027-06-01"}}
```

```ini
Address        = https://vendor.example/api/licence
Method         = POST
Domain field   = domain
Extra fields   = product: sample-pro
Headers        = Authorization: Bearer <your token>
Expected status = 200
Body type      = JSON
Path           = data.valid
Expected value = true
```

The same server can answer in plain text instead. Then the body type is text, the path stays empty, and the expected value is the word itself, such as `OK`.

Inside the product, the same question is asked with the carrier. The answer is the raw response, so your code decides what it means.

```php
$answer = \License::remote_check('https://vendor.example/api/licence', [
    'product' => 'sample-pro',
    'domain'  => \Utility::getDomain(),
]);

// Unreachable is not a refusal: do not lock a customer out over your own downtime.
if ($answer === null) return true;

$body = \Utility::jdecode((string) ($answer['body'] ?? ''), true);

return (int) ($answer['status'] ?? 0) === 200 && ($body['data']['valid'] ?? false) === true;
```

## Pitfalls

> **Silence is not permission**
> 
> Once a check is declared, an unreachable server refuses the download. You said "ask me", and no answer is not a yes.

> **The answer is taken at its word**
> 
> This call is made from the panel to your server, and the customer who would gain from a forged yes is not on that path. A signature in the answer is not checked.

> **Editing the declaration drops the proof**
> 
> A stamp says "the declaration stored right now answers". Change a header, a path or the address, and the section returns to incomplete until you test again.

## Related Articles

- [Publishing a Product](https://dev.wisecp.com/en/publishing-a-marketplace-product)
- [Shipping a Release](https://dev.wisecp.com/en/shipping-a-marketplace-release)
- [Licensing a Module](https://dev.wisecp.com/en/licensing-a-module)
