Skip to content

Set up SunLicense over HTTP ​

The agent prompt uses the CLI (sunlicense init). When nothing can be installed or run (a browser builder such as Lovable, Bolt or Replit, a locked-down sandbox, an agent without a shell), do the same setup with plain HTTPS calls: the steps below, in whatever the environment has (curl, PowerShell, or the project's own language). Then go back to the prompt's step 3.

StepCallKeep
1. Guest accountPOST /api/v1/auth/guesttoken (slp_…), claimUrl
2. ProductPOST /api/v1/productsid
3. PolicyPOST /api/v1/policiesid
4. API keyPOST /api/v1/api-keyskey (slk_…), shown once
5. Test licensePOST /api/v1/licensesid, key
CheckPOST /api/v2/license/validatevalid, code
Suspend, reinstatePOST /api/v1/licenses/{id}/suspend, /reinstate

Base URL https://api.sunlicense.com (or SUNLICENSE_SERVER when it's set). Every call after the first sends Authorization: Bearer <token> and Content-Type: application/json. Errors are JSON with a detail sentence and often a code.

Keys and tokens: write the API key and the test license key to the project's gitignored .env (add .env to .gitignore if it isn't there) as SUNLICENSE_API_KEY and SUNLICENSE_LICENSE_KEY; never into source code, logs or messages. On a browser builder, put them in the platform's secrets settings instead. The guest token (slp_) can manage the whole organization: keep it out of the app (only the API key ships) and give it to the developer once, at the end, with the claim link.

curl (macOS, Linux, WSL, Git Bash) ​

bash
API=${SUNLICENSE_SERVER:-https://api.sunlicense.com}
J='Content-Type: application/json'

# 1. A guest account (no email; the developer claims it later). 201, or 429 guest_limit / 403 guest_unavailable: then sign up instead.
GUEST=$(curl -sS -X POST "$API/api/v1/auth/guest" -H "$J" -d '{"name": "My App", "client": "agent-http"}')
TOKEN=$(printf %s "$GUEST" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
A="Authorization: Bearer $TOKEN"

# 2. A product (the app you sell)
PRODUCT=$(curl -sS -X POST "$API/api/v1/products" -H "$A" -H "$J" -d '{"name": "My App"}' | sed -n 's/^{"id":"\([^"]*\)".*/\1/p')

# 3. A policy: subscription, 2 machines, 365 days
POLICY=$(curl -sS -X POST "$API/api/v1/policies" -H "$A" -H "$J" \
  -d "{\"productId\": \"$PRODUCT\", \"name\": \"Standard\", \"type\": \"SUBSCRIPTION\", \"maxMachines\": 2, \"durationDays\": 365}" \
  | sed -n 's/^{"id":"\([^"]*\)".*/\1/p')

# 4. An API key for the app (it can only check licenses, so it may ship inside the app)
APIKEY=$(curl -sS -X POST "$API/api/v1/api-keys" -H "$A" -H "$J" -d '{"name": "My App"}' | sed -n 's/.*"key":"\([^"]*\)".*/\1/p')

# 5. A test license (never emailed)
LICENSE=$(curl -sS -X POST "$API/api/v1/licenses" -H "$A" -H "$J" \
  -d "{\"policyId\": \"$POLICY\", \"customerEmail\": \"[email protected]\", \"customerName\": \"Test\"}")
LICENSE_ID=$(printf %s "$LICENSE" | sed -n 's/^{"id":"\([^"]*\)".*/\1/p')
LICENSE_KEY=$(printf %s "$LICENSE" | sed -n 's/.*"key":"\([^"]*\)".*/\1/p')

printf 'SUNLICENSE_API_KEY=%s\nSUNLICENSE_LICENSE_KEY=%s\n' "$APIKEY" "$LICENSE_KEY" >> .env
grep -qx '.env' .gitignore 2>/dev/null || echo .env >> .gitignore

# Check it the way the app will: 200 {"valid": true, "code": "VALID"}. Changes reach license checks within seconds,
# so a brand-new license can answer NOT_FOUND at first: wait and check again.
check() { curl -sS -X POST "$API/api/v2/license/validate" -H "Authorization: Bearer $APIKEY" -H "$J" \
  -d "{\"license\": \"$LICENSE_KEY\", \"host\": \"agent-test\"}"; echo; }
sleep 5; check
curl -sS -X POST "$API/api/v1/licenses/$LICENSE_ID/suspend" -H "$A" -o /dev/null
sleep 5; check      # 400 {"valid": false, "code": "SUSPENDED"}; can take a few seconds
curl -sS -X POST "$API/api/v1/licenses/$LICENSE_ID/reinstate" -H "$A" -o /dev/null
sleep 5; check      # VALID again

jq is easier than sed where it's installed (jq -r .token).

PowerShell (Windows) ​

powershell
$api = if ($env:SUNLICENSE_SERVER) { $env:SUNLICENSE_SERVER } else { 'https://api.sunlicense.com' }
function Call($path, $body, $token) {
  $h = @{}; if ($token) { $h.Authorization = "Bearer $token" }
  Invoke-RestMethod -Method Post -Uri "$api$path" -Headers $h -ContentType 'application/json' -Body ($body | ConvertTo-Json)
}

$guest   = Call '/api/v1/auth/guest' @{ name = 'My App'; client = 'agent-http' }
$t       = $guest.token
$product = Call '/api/v1/products' @{ name = 'My App' } $t
$policy  = Call '/api/v1/policies' @{ productId = $product.id; name = 'Standard'; type = 'SUBSCRIPTION'; maxMachines = 2; durationDays = 365 } $t
$apiKey  = Call '/api/v1/api-keys' @{ name = 'My App' } $t
$license = Call '/api/v1/licenses' @{ policyId = $policy.id; customerEmail = '[email protected]'; customerName = 'Test' } $t

Add-Content .env "SUNLICENSE_API_KEY=$($apiKey.key)`nSUNLICENSE_LICENSE_KEY=$($license.key)"
if (-not ((Test-Path .gitignore) -and (Select-String -Quiet -Pattern '^\.env$' .gitignore))) { Add-Content .gitignore '.env' }

function Check {
  try { Call '/api/v2/license/validate' @{ license = $license.key; host = 'agent-test' } $apiKey.key }
  catch { $_.ErrorDetails.Message }   # a rejected key answers HTTP 400 with the reason
}
Start-Sleep 5; Check                                      # valid: True, code: VALID (a brand-new license can be NOT_FOUND for a few seconds)
Call "/api/v1/licenses/$($license.id)/suspend" @{} $t | Out-Null
Start-Sleep 5; Check                                      # "valid":false, "code":"SUSPENDED"
Call "/api/v1/licenses/$($license.id)/reinstate" @{} $t | Out-Null
Start-Sleep 5; Check                                      # VALID again

JavaScript (Node.js 18+, Deno, Bun) ​

js
// setup.mjs: node setup.mjs, then delete it
import { appendFileSync } from 'node:fs';

const api = process.env.SUNLICENSE_SERVER ?? 'https://api.sunlicense.com';
const call = async (path, body, token) => {
  const res = await fetch(api + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }) },
    body: JSON.stringify(body ?? {}),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok && path !== '/api/v2/license/validate') throw new Error(`${path}: ${res.status} ${data.detail ?? ''}`);
  return data;
};

const { token, claimUrl } = await call('/api/v1/auth/guest', { name: 'My App', client: 'agent-http' });
const product = await call('/api/v1/products', { name: 'My App' }, token);
const policy = await call('/api/v1/policies', { productId: product.id, name: 'Standard', type: 'SUBSCRIPTION', maxMachines: 2, durationDays: 365 }, token);
const apiKey = await call('/api/v1/api-keys', { name: 'My App' }, token);
const license = await call('/api/v1/licenses', { policyId: policy.id, customerEmail: '[email protected]', customerName: 'Test' }, token);
appendFileSync('.env', `SUNLICENSE_API_KEY=${apiKey.key}\nSUNLICENSE_LICENSE_KEY=${license.key}\n`);

const check = () => call('/api/v2/license/validate', { license: license.key, host: 'agent-test' }, apiKey.key).then((r) => r.code);
const wait = () => new Promise((r) => setTimeout(r, 5000));
await wait(); console.log(await check());                                     // VALID (a brand-new license can be NOT_FOUND for a few seconds)
await call(`/api/v1/licenses/${license.id}/suspend`, null, token); await wait();
console.log(await check());                                                   // SUSPENDED
await call(`/api/v1/licenses/${license.id}/reinstate`, null, token); await wait();
console.log(await check());                                                   // VALID
console.log('Claim the account:', claimUrl);

Add .env to .gitignore if it isn't there.

Then ​

  • Add the check to the app (the prompt's step 3): the official SDK for the language, or POST /api/v2/license/validate with {"license": "<key>", "host": "<stable machine id>"} and the API key as a Bearer token. 200 is valid; 400 is rejected, with code (NOT_FOUND, EXPIRED, SUSPENDED, REVOKED, TOO_MANY_MACHINES, …); no answer or 5xx: keep working (fail open). More: Validation API.
  • MCP: without the CLI, connect the editor to the remote server https://api.sunlicense.com/mcp (remote MCP) once the account is claimed and the developer can sign in.
  • Tell the developer: where the license key goes, how to issue, suspend and reinstate keys in the dashboard, and the claim link (claimUrl, valid 7 days; a fresh one: Guest accounts). An unclaimed guest account is deleted 30 days after it was last used. Hand over the guest token once, to keep in a password manager: it signs the CLI in with sunlicense login --with-token.

A guest account can hold 10 licenses and can't send email or host files until it's claimed (what it can do).

Send feedback

Found something broken, or have an idea? It goes straight to our board.

The first line becomes the title.
Images (optional, up to 5: paste, drop or capture)
Check each image before sending, and remove any that show something private.

We also receive this page's address and your browser version.

Ctrl Enter to send