---
url: https://docs.sunlicense.com/guide/agent-setup.md
description: >-
  What sunlicense init does, as plain HTTPS calls - a guest account, a product,
  a policy, an API key and a test license, then validate, suspend and reinstate.
  In curl, PowerShell and JavaScript, for agents and tools that can't run the
  CLI.
---

# Set up SunLicense over HTTP

[The agent prompt](https://sunlicense.com/prompt) uses the [CLI](/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.

| Step | Call | Keep |
|---|---|---|
| 1. Guest account | `POST /api/v1/auth/guest` | `token` (`slp_…`), `claimUrl` |
| 2. Product | `POST /api/v1/products` | `id` |
| 3. Policy | `POST /api/v1/policies` | `id` |
| 4. API key | `POST /api/v1/api-keys` | `key` (`slk_…`), shown once |
| 5. Test license | `POST /api/v1/licenses` | `id`, `key` |
| Check | `POST /api/v2/license/validate` | `valid`, `code` |
| Suspend, reinstate | `POST /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\": \"test@example.com\", \"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 = 'test@example.com'; 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: 'test@example.com', 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](/guide/sdks) 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](/api/validation).
* **MCP:** without the CLI, connect the editor to the remote server `https://api.sunlicense.com/mcp`
  ([remote MCP](/guide/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](/guide/guest-accounts#claim-it)).
  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](/guide/guest-accounts#what-a-guest-account-can-do)).
