Ulises LicensesUlises Licenses
FeaturesPricingAPIAnnouncements
Solutions
Java & Kotlin LicensingMinecraft Plugin LicensingChoosing LicensingBuiltByBit IntegrationPolymart Integration
Discord
Dashboard
  • Overview
Using the SDK
  • Installation
  • Plugin Integration
  • Complete Example
  • API Methods
  • Connection Management
  • Auto-Update
  • Best Practices
Using the Endpoint
  • Endpoint Overview
  • Validate a License
  • Verify the Response
  • Endpoint Best Practices
  1. Docs
  2. /
  3. Validate a License

POST /api/endpoint/validate/license

One POST endpoint does everything: it authenticates your product key, loads the license, applies every enforcement rule, binds a new HWID or IP if there is room, and returns the result with a signed token.

Request

Method
POST
URL
https://www.uliseslicenses.xyz/api/endpoint/validate/license
Content type
application/json
Response
application/json
Request
curl
1curl -i -X POST https://www.uliseslicenses.xyz/api/endpoint/validate/license \2  -H "Content-Type: application/json" \3  -H "Accept: application/json" \4  -d '{5  "apiKey": "ulises_3f9a1c7e5b2d40819ac6e3f7b0d5a812",6  "productId": "65f1c2a4b9d84e37a0c5119f",7  "licenseKey": "ULS-XXXX-XXXX-XXXX",8  "hwid": "9A2B7C1D-44E5-4F60-8B9A-0C1D2E3F4A5B",9  "nonce": "b3f1c8d2a9e04756",10  "version": "1.4.2"11}'

Request fields

FieldTypeRequiredMeaning
apiKeystringyesPer-product key, format ulises_ followed by 32 lowercase hex characters. It identifies the product and authenticates the developer at the same time. It is not the user credential, and it is never issued to your buyers.
productIdstringyesThe product id. It must match the product the apiKey belongs to, otherwise the request is rejected with Product does not match the API key.
licenseKeystringyesThe end user's license key, exactly as they entered it in your plugin.
hwidstringconditionalHardware id of the machine running the plugin. Required when the product has HWID enforcement enabled. Accepted charset is A-Fa-f0-9:- with 8 to 128 characters, and it is case insensitive because the value is uppercased server side.
ipstringnoThe public IP the license should be bound to. When omitted the server uses the source address it observed on the connection.
noncestringno8 to 128 characters from A-Za-z0-9._:- . When present it is single use: a second request reusing it is rejected with 409 Replay detected. The server always returns a nonce in the response, whether you sent one or not.
versionstringnoThe plugin version. Optional, used only for telemetry.
NOTICE: License keys never go in a query string
This endpoint is POST only. A license key in the URL ends up in proxy logs, CDN logs and browser history, which is how credentials leak.

Successful response

HTTP 200 with success: true. A 200 is the only response that means the license passed, so a token is always present.

200 OK
JSON
1{2  "success": true,3  "message": "Valid license",4  "type": "lifetime",5  "status": "active",6  "expiresAt": "2027-01-01T00:00:00.000Z",7  "data": {8    "product": {9      "id": "65f1c2a4b9d84e37a0c5119f",10      "name": "MyBestPlugin",11      "description": "Anti cheat suite for survival servers"12    },13    "license": {14      "discordId": "1216532655592439862",15      "resource": "MyBestPlugin",16      "licenseKey": "ULS-XXXX-XXXX-XXXX",17      "issuedBy": "1216532655592439862",18      "expiresAt": "2027-01-01T00:00:00.000Z",19      "status": "active",20      "ipLimit": 3,21      "ips": [],22      "subType": "lifetime",23      "hwid": "9A2B7C1D-44E5-4F60-8B9A-0C1D2E3F4A5B"24    },25    "updateInfo": {26      "available": true,27      "version": "1.2.3",28      "jarUrl": null29    }30  },31  "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IlVMUy1MSUMiLCJraWQiOiIyMDI2LTEwIn0.eyJ2IjoyLCJpc3MiOiJodHRwczovL3d3dy51bGlzZXNsaWNlbnNlcy54eXoiLCJhdWQiOiI2NWYxYzJhNGI5ZDg0ZTM3YTBjNTExOWYiLCJqdGkiOiJiM2YxYzhkMmE5ZTA0NzU2IiwiaWF0IjoxNzY3MjI1NjAwLCJuYmYiOjE3NjcyMjUzMDAsImV4cCI6MTc2NzI0MDAwMCwiaHdpZCI6IjlBMkI3QzFELTQ0RTUtNEY2MC04QjlBLTBDMUQyRTNGNEE1QiIsInN0IjoiYWN0aXZlIiwiZGF0YSI6eyJwcm9kdWN0Ijp7ImlkIjoiNjVmMWMyYTRiOWQ4NGUzN2EwYzUxMTlmIiwibmFtZSI6Ik15QmVzdFBsdWdpbiIsImRlc2NyaXB0aW9uIjoiQW50aSBjaGVhdCBzdWl0ZSBmb3Igc3Vydml2YWwgc2VydmVycyJ9LCJsaWNlbnNlIjp7ImRpc2NvcmRJZCI6IjEyMTY1MzI2NTU1OTI0Mzk4NjIiLCJyZXNvdXJjZSI6Ik15QmVzdFBsdWdpbiIsImxpY2Vuc2VLZXkiOiJVTFMtWFhYWC1YWFhYLVhYWFgiLCJpc3N1ZWRCeSI6IjEyMTY1MzI2NTU1OTI0Mzk4NjIiLCJleHBpcmVzQXQiOiIyMDI3LTAxLTAxVDAwOjAwOjAwLjAwMFoiLCJzdGF0dXMiOiJhY3RpdmUiLCJpcExpbWl0IjozLCJpcHMiOltdLCJzdWJUeXBlIjoibGlmZXRpbWUiLCJod2lkIjoiOUEyQjdDMUQtNDRFNS00RjYwLThCOUEtMEMxRDJFM0Y0QTVCIn0sInVwZGF0ZUluZm8iOnsiYXZhaWxhYmxlIjp0cnVlLCJ2ZXJzaW9uIjoiMS4yLjMiLCJqYXJVcmwiOm51bGx9fX0.TkJ_3UZLWelVvv0S0_PcVJlRTjWuG7as3EB_cbA_XYFQvp5EoG1sWAF5FiRyXsr3Go36BFpye3h-UlHeLb2e2Q",32  "nonce": "b3f1c8d2a9e04756"33}
  • type: The license tier label, for example lifetime or enterprise. The SDK exposes the same value as subType.
  • data: Exactly the tree UlisesLib returns, so existing parsing code and field names carry over unchanged.
  • ips: Always an empty array. The server never discloses which addresses are bound to a license, so nothing here leaks another customer's infrastructure.
  • updateInfo: Null when the product has no published version. When it is present it tells you a version exists and what it is. Downloading it is not part of this contract.
  • token: The compact JWS. Verify it before you trust anything you parsed. See Verifying the Response.
  • nonce: Always present, and always the value that is now consumed. Echo it into your logs for support, never a license key.

Calling it from Java

java.net.http is in the JDK, and Gson ships with every PaperMC and Spigot server, so this needs no dependency at all. Build the nonce fresh for every attempt, and verify the returned token before you act on anything you parsed.

LicenseCheck.java
Java
1import com.google.gson.JsonObject;2import com.google.gson.JsonParser;3import java.net.URI;4import java.net.http.HttpClient;5import java.net.http.HttpRequest;6import java.net.http.HttpResponse;7import java.security.SecureRandom;8import java.time.Duration;9import java.util.HexFormat;10 11String url = "https://www.uliseslicenses.xyz/api/endpoint/validate/license";12 13byte[] noise = new byte[8];14new SecureRandom().nextBytes(noise);15String nonce = HexFormat.of().formatHex(noise);16 17String body = "{"18    + "\"apiKey\":\"" + apiKey + "\","19    + "\"productId\":\"" + productId + "\","20    + "\"licenseKey\":\"" + licenseKey + "\","21    + "\"hwid\":\"" + hwid + "\","22    + "\"nonce\":\"" + nonce + "\","23    + "\"version\":\"" + pluginVersion + "\""24    + "}";25 26HttpClient client = HttpClient.newBuilder()27    .connectTimeout(Duration.ofSeconds(5))28    .build();29 30HttpRequest request = HttpRequest.newBuilder()31    .uri(URI.create(url))32    .timeout(Duration.ofSeconds(10))33    .header("Content-Type", "application/json")34    .header("Accept", "application/json")35    .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))36    .build();37 38HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());39 40if (response.statusCode() == 200) {41    JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();42    String token = json.get("token").getAsString();43    if (LicenseTokenVerifier.verify(token, productId, hwid)) {44        startPlugin(json);45    }46} else {47    String message = JsonParser.parseString(response.body()).getAsJsonObject().get("message").getAsString();48    getLogger().warning("License check failed: " + message);49}

Error responses

Every error is success: false with a message and, where it helps your code branch, one extra flag.

StatusmessageExtra flag
400Invalid request bodynone
400Missing required fieldsnone
400License has expirednone
400HWID is requiredrequiresHwid: true
401Invalid API keynone
403Product is not activenone
403Product does not match the API keynone
403HWID limit reachedhwidMismatch: true
403IP limit reachedipMismatch: true
404No valid license foundnone
409Replay detectednone
429Daily quota exceededquotaExceeded: true, plus X-Quota-* and Retry-After headers
429Rate limit exceededX-RateLimit-Limit, X-RateLimit-Remaining, Retry-After headers
500Internal server errornone

Rate limits and the daily quota

Every response carries your remaining allowance. A 429 never means the license is bad, so wait for Retry-After and retry with exponential backoff.

HeaderMeaning
X-Quota-LimitThe daily request allowance for the plan that owns this product. The literal string unlimited for Pro.
X-Quota-RemainingRequests left in the current window. Unlimited for Pro.
X-Quota-ResetSeconds until the window resets. Absent for Pro.
ScopeLimitApplies to
Daily quota per API key4,000 requests/dayRolling 24 hour window starting at your first request of the window, not a calendar day, so you cannot double up around midnight. Pro plans have no limit.
Per license key30 requests/minuteRolling window, regardless of which product or API key it arrives with.
Per observed source IP300 requests/minuteApplied before anything else, as a flood guard.

Caching

Every response carries Cache-Control: no-store. Do not put an intermediary in front of this endpoint, and do not let your HTTP client reuse a cached response for a different license.

Caching in your own plugin is a different thing and you should do it. Validate on startup, then reuse the result for a minute or two before asking again. On a Free plan that is the difference between a few hundred requests a day and several thousand, and it keeps you clear of the quota on a busy server.

Prefer a bundled library?

UlisesLib.jar does the same validation for you, with retries, HWID collection and the auto-updater built in. It is a dependency you shade into your jar rather than a contract you implement.

Using the SDK

Endpoint reference questions

Where do I get the apiKey?

From your product page in the developer dashboard. It is per product, it starts with ulises_ followed by 32 hex characters, and it authenticates you rather than the buyer.

Can I put the license key in the URL?

No. The endpoint is POST only and takes a JSON body. License keys never belong in a query string, where they end up in proxy logs and browser history.

Does every response carry a token?

Only successful ones. Every 200 carries a token, and only a 200 means the license passed, so verifying the token is a second gate on the same decision.

What happens on the first run of a new machine?

The first request binds the HWID to the license if the license has room under its HWID limit. If it does not, you get 403 HWID limit reached with hwidMismatch set.

How should I handle a 429?

Wait for Retry-After seconds, or back off exponentially starting around one second. A 429 is transient and never means the license is bad.

PreviousEndpoint OverviewNextVerify the Response

Built by Ulises - Citymoon Dynamics

Ulises LicensesUlises Licenses

License management for Minecraft plugins and Java applications.

Discord

Legal

  • Terms of Service
  • Privacy Policy
  • Refund Policy
  • DPA

Docs

  • Overview
  • Installation
  • Plugin Integration
  • Complete Example
  • API Methods
  • Connection Management
  • Auto-Update
  • Best Practices
  • Endpoint Overview
  • Validate a License
  • Verify the Response
  • Endpoint Best Practices

History

  • Announcements
  • Citymoon Dynamics

Company

  • About
  • Security
  • Status
  • Contact

Solutions

  • Java & Kotlin
  • Minecraft Plugins
  • Choosing Licensing
  • BuiltByBit
  • Polymart

© 2025-2030 Citymoon Dynamics (citymoon.org). All rights reserved.