The iRonShore Lighthouse API
Scripts, pipelines and other programs use the same API as the Lighthouse console, under /api/v1. Version 1 only grows: routes and fields are added, never removed or changed.
- API Explorer:
https://<your server>/api/v1/docs(API next to Help at the top of every console page, opening in a tab of its own so the console page stays as it was), read from the OpenAPI 3.1 description at/api/v1/openapi.json. Each route has Try it, which sends the request from the browser as the person signed in to the console, or with an API token pasted at the top of the page. Import the OpenAPI file into Postman, a client generator or PowerShell tooling. - Errors are
{"error": "..."}with 400 (not valid), 403 (not allowed, naming the permission), 404 (not found, or not visible to the caller), 409, 422 (a template's policy) or 502 (the CA or mail server said no). - A certificate the caller may not see answers 404, not 403, so its existence is not confirmed.
- Settings routes need the permission of their group: administrators (the whole Administrator profile everywhere, the Operator role in
GET /me) and anyone given that group's permission everywhere. Connections (settings.connections):/settings/cas, adding, changing or deleting/templatesand/facts,/settings/integrations,/settings/webhooks,/settings/log-destinations,/settings/ai-model,/settings/password-managers,/settings/secret-pointers,/outposts. People (settings.people):/access/..., every team,/settings/signin,/settings/mail,/settings/approval-email,/settings/message-wording,/settings/team-summaries. Bulk changes (settings.bulk):/drills,/ca-migrations,/moves. Logs (settings.logs):/settings/server-log, and reading/auditand/logs. Server (settings.server):/settings/general,/settings/site-name,/settings/banner,/settings/notice,/settings/weak-rules,/settings/crypto-policy,/settings/key-types,/settings/subject-defaults,/settings/pfx-downloads,/settings/key-encryption,/settings/personal-acme-keys,/settings/ai-suggestions,/settings/features/{key},/settings/license,/settings/cleanup,/servers,PUT /dashboard-layout,GET /dashboard-layout/teams.GET /settingsandGET /serveranswer anyone who opens any group. Anyone else gets 403 naming the permission.GET /mereturnsareas, the groups the caller opens (connections,people,bigjobs,records,server). Reading/templates,/facts,/authorities,/featuresand/bannerstays open, since enrolling needs them, and the records/audit,/logsand/keyskeep their own permissions too.settings.manage,access.manageandtemplates.manageare gone; a profile sent with them drops them. - You give only what you hold, and never to yourself. Creating or changing a grant, profile, team's members, Spotlight's Who can use it,
/access/automation,/access/new-people, an API token's profile, or a template'speople, answers 403 when the caller does not hold what it gives, where it gives it (removing or narrowing access needs what it gives now). When the change names the caller (their account, a group of theirs, a team of theirs, or the token itself), it is not made: the answer is202 Acceptedwith{"request": {...}, "waits": "..."}, and the change waits on Requests as a request of kindaccessfor someone else who opens that group under Settings. Grants, team members, Spotlight shares,/access/automationentries and template people takeuntil(UTC; it must be ahead), after which they stop counting and are removed within the hour. - Lists show only what the caller sees: for anyone but an administrator,
GET /ownerslists the teams they are in and the teams owning certificates they see (another team with only itsidandname; everyone given See certificates or Approve requests everywhere gets every team),GET /tagsandGET /authoritiesonly those on or behind certificates they see,GET /change-windowsonly their teams' windows, and every count is over the certificates they see.GET /templateslists only the templates opened to them, with only their own teams inteamsand nopeople. - Alerts are for administrators and people given an alerts permission:
GET /alerts/recentneeds See alerts (alerts.view; everywhere gives every alert, for some teams only alerts about those teams' certificates) or Create and change alerts (alerts.manage, every alert); anyone else gets 403.GET /jobslists the alert jobs left from before 0.33.0 (expiry-alerts,weak-check) only to them, and changing, running or deleting one needs Create and change alerts, not Create and change jobs;POST /jobsadds no new one (400), since alerts are Pipelines now. The same two permissions openGET /pipelineswith only thecertificate.expiringandcertificate.weakevents and their Pipelines, andGET /pipelines/runswith only their runs (See alerts for some teams gets no runs); Create and change alerts also adds, changes, orders and deletes those Pipelines withtellandwaitsteps. No built-in profile but Administrator has either permission. - Reports are for administrators and people given a reports permission:
GET /reports,GET /reports/{kind}andGET /reports/{kind}/csvneed See reports (reports.view; everywhere reports on every certificate, for some teams only on those teams' certificates and the Spotlights the caller may see) or Create and change reports (reports.manage, every certificate); anyone else gets 403. Saving, changing, deleting and sending saved reports needs Create and change reports. No built-in profile but Administrator has either permission. - The Keeper's Log is for administrators, Settings, Logs and auditors:
GET /audit(the Keeper's Log, Lighthouse's audit log) needs the Administrator profile, Read the logs (settings.logs) or Read the Keeper's Log (audit.view) everywhere; the built-in Auditor profile has the last.POST /audit/checkchecks every sealed entry against its seal and answers{ok, checked, sealedUpTo, waiting, checkedAt, problems: [{auditEventId, kind, what}], summary, keyMissing}, wherekindischanged,removedoradded.?kind=sign-insshows only sign-ins and sign-outs (sign-in,sign-out,sign-in-failed,sign-in-refused,first-sign-in,token-use),?kind=settingsonlysettings-change, whosedetailsis JSON withbeforeandafter. A change through the API whose route wrote nothing is still recorded, aschangewith its method and path. To keep a copy outside Lighthouse, use log forwarding'sauditstream.
Signing in
API tokens are for anything that runs unattended. On People & Access, under API tokens, choose New API token, give it a name such as deploy-pipeline, a lifetime (up to two years) and a profile, everywhere or for some teams. The secret is shown once; Lighthouse keeps only its hash.
Send it on every request:
Authorization: Bearer lh_...
A token is its own name, token:<name>. It can do only what is granted to that name on People & Access, exactly like a person: give it more with Grant…, or add token:<name> to a team as a member. Keeper's Log entries show it as token:<name>, with token-use the first time it is used after 8 quiet hours, and certificates it enrolls are owned by it and the team it enrolled for. New secret replaces the secret and keeps the grants; Remove stops it at once and removes its grants.
Too many bad tokens. An address that sends 20 unknown, removed or expired tokens in 5 minutes gets 429 Too Many Requests (with Retry-After) for every token, good ones too, until the 5 minutes pass. Requests with a good token are not limited; /mcp is, at 120 a minute per address.
Windows sign-in also works, for a script run by a person on a domain computer (Invoke-RestMethod -UseDefaultCredentials). Requests that change something then need the header X-Lighthouse: 1; with a token they do not.
In IIS, keep Anonymous Authentication on for the site (the default) beside Windows Authentication, so a request with a token reaches Lighthouse.
No session. Without a token or a session, the API answers 401 with the header X-Lighthouse-Sign-In: /signin. A request that carries X-Lighthouse-Console: 1, as the console and the API explorer send, gets 440 (Login Time-out) instead, with the same header: IIS adds its Windows challenge to every 401, and a browser would answer that with its own password box. Scripts should not send it. After a session timed out the header says why, such as /signin?signedOut=true&ended=idle (or ended=longest), and the console goes there.
Session timeouts. A browser session ends after Settings, Sign-in, Sessions' minutes without use (30 unless changed) and its longest session (12 hours). Every request with the session cookie counts as use unless it carries X-Lighthouse-Background: 1, which the console sends on what it reads by itself, such as Server Log's live lines; POST /me/active is the console saying someone is typing, clicking or scrolling. GET /me returns session (idleMinutes, warnSeconds, longestHours, endsAt). API tokens have no session and never time out this way.
Examples
PowerShell:
$h = @{ Authorization = "Bearer $env:LIGHTHOUSE_TOKEN" }
$lh = "https://lighthouse.example.test/api/v1"
# What expires in the next 30 days, and who owns it
Invoke-RestMethod "$lh/certificates?expiring=30" -Headers $h | Select-Object displayName, daysLeft, owners
# Every certificate with no owner, 500 at a time (X-Total-Count says how many in all)
$offset = 0
do {
$page = Invoke-RestMethod "$lh/certificates?rule=owner:none&sort=notAfter&offset=$offset&limit=500" -Headers $h -ResponseHeadersVariable rh
$page | Select-Object displayName, notAfter
$offset += 500
} while ($offset -lt [int]$rh["X-Total-Count"][0])
# Fill in a Fact on certificates that don't have it yet, from a CSV with columns thumbprint,costCenter.
# PUT .../facts replaces all of a certificate's facts, so send its current ones back with the new one.
$missing = Invoke-RestMethod "$lh/certificates?rule=$([uri]::EscapeDataString('not fact:Cost center'))&limit=2000" -Headers $h
$wanted = @{}; Import-Csv costcenters.csv | ForEach-Object { $wanted[$_.thumbprint.ToUpper()] = $_.costCenter }
foreach ($c in $missing | Where-Object { $wanted.ContainsKey($_.thumbprint.ToUpper()) }) {
$facts = @{}; $c.facts.PSObject.Properties | ForEach-Object { $facts[$_.Name] = $_.Value }
$facts["Cost center"] = $wanted[$c.thumbprint.ToUpper()]
Invoke-RestMethod "$lh/certificates/$($c.thumbprint)/facts" -Method Put -Headers $h -ContentType "application/json" -Body (@{ facts = $facts } | ConvertTo-Json)
}
# Enroll a CSR made on the server that will use the certificate
$body = @{ templateId = 3; team = 7; csr = Get-Content .\web01.csr -Raw } | ConvertTo-Json
Invoke-RestMethod "$lh/enroll" -Method Post -Headers $h -ContentType application/json -Body $body
# Run an installation's renewal job now
Invoke-RestMethod "$lh/jobs/12/run" -Method Post -Headers $h
# Keep an F5 BIG-IP client SSL profile's certificate current (administrators; Network appliances on).
# The password is stored on the server first: lh secret set appliance-password
$job = @{ name = "bigip01 www"; kind = "renew-appliance"; schedule = "daily 03:00"; autoRenew = "renew"
parameters = @{ vendor = "f5"; host = "bigip01.example.test"; user = "lighthouse"; profile = "clientssl-www"
templateId = 3; servedHost = "www.example.test"; syncGroup = "failover-group" } } | ConvertTo-Json
Invoke-RestMethod "$lh/jobs" -Method Post -Headers $h -ContentType application/json -Body $job
# Keep SQL Server's certificate current (administrators); the instance restarts to take it, so give it a change window
$job = @{ name = "sql01 MSSQLSERVER"; kind = "renew-windows"; schedule = "daily 03:00"; autoRenew = "renew"; changeWindow = "sun 02:00-04:00"
parameters = @{ host = "sql01.example.test"; use = "sql"; instance = "MSSQLSERVER"; templateId = 3 } } | ConvertTo-Json
Invoke-RestMethod "$lh/jobs" -Method Post -Headers $h -ContentType application/json -Body $job
# Keep an IIS site current, signing in as a separate install account rather than Lighthouse's own (administrators);
# its password is stored first on the server (lh secret set windows-web-servers) or, from an Outpost, in that Outpost's Secrets
$job = @{ name = "web01 Default Web Site"; kind = "renew-iis"; schedule = "daily 03:00"
parameters = @{ host = "web01.example.test"; site = "Default Web Site"; templateId = 3
signInAs = "EXAMPLE\lh-install-web"; signInPasswordSecret = "windows-web-servers" } } | ConvertTo-Json
Invoke-RestMethod "$lh/jobs" -Method Post -Headers $h -ContentType application/json -Body $job
# Keep a Tomcat PFX on Windows current and restart Tomcat (administrators); the password is stored first: lh secret set installation-app01
$job = @{ name = "app01 Tomcat"; kind = "renew-windows-files"; schedule = "daily 03:00"
parameters = @{ host = "app01.example.test"; format = "pfx"; pfxPath = "C:\Tomcat\conf\tls.pfx"; pfxPasswordSecret = "installation-app01"
service = "Tomcat9"; servedHost = "app01.example.test"; servedPort = 8443; templateId = 3 } } | ConvertTo-Json
Invoke-RestMethod "$lh/jobs" -Method Post -Headers $h -ContentType application/json -Body $job
# Add a HashiCorp Vault PKI CA (administrators only); the AppRole secret id goes to the secret store
$ca = @{ kind = "vault"; name = "Lab Vault"; apiKey = $env:VAULT_SECRET_ID
settings = @{ url = "https://vault.example.test:8200"; mount = "pki_int"; authMethod = "approle"; roleId = "<role id>" } } | ConvertTo-Json
Invoke-RestMethod "$lh/settings/cas/test" -Method Post -Headers $h -ContentType application/json -Body $ca
Invoke-RestMethod "$lh/settings/cas" -Method Post -Headers $h -ContentType application/json -Body $ca
# Let a team's Installations renew by themselves (off, notify or renew; $null uses the global setting)
Invoke-RestMethod "$lh/owners/7/auto-renew" -Method Put -Headers $h -ContentType application/json -Body '{"autoRenew":"renew"}'
# Give a Spotlight to a person and a team (administrators only); the list replaces who may use it
$ro = (Invoke-RestMethod "$lh/access/profiles" -Headers $h | Where-Object builtIn -eq "read-only").id
$who = ConvertTo-Json -InputObject @(@{ principal = "EXAMPLElice"; profileId = $ro }, @{ team = 7; profileId = $ro })
Invoke-RestMethod "$lh/spotlights/12/access" -Method Put -Headers $h -ContentType application/json -Body $who
# Spotlights that need someone: expired or due within 7 days
Invoke-RestMethod "$lh/spotlights/health" -Headers $h | Where-Object status -eq "bad" | Select-Object name, team, expired, within7, within30
# Replace-All Drill: dry run for a CA, read the result, then replace them all
$drill = Invoke-RestMethod "$lh/drills" -Method Post -Headers $h -ContentType application/json -Body (@{ ca = "Lab Issuing CA" } | ConvertTo-Json)
(Invoke-RestMethod "$lh/drills/$($drill.id)" -Headers $h).dryRun.items | Select-Object name, status, detail
Invoke-RestMethod "$lh/drills/$($drill.id)/replace" -Method Post -Headers $h
# CA migration: check what the ADCS CA issued against an EJBCA template, move what is ready, then revoke the old ones
$m = Invoke-RestMethod "$lh/ca-migrations" -Method Post -Headers $h -ContentType application/json -Body (@{ fromCa = "Lab ADCS CA"; toTemplateId = 7 } | ConvertTo-Json)
(Invoke-RestMethod "$lh/ca-migrations/$($m.id)" -Headers $h).check.items | Select-Object subject, status, detail
Invoke-RestMethod "$lh/ca-migrations/$($m.id)/move" -Method Post -Headers $h
$n = (Invoke-RestMethod "$lh/ca-migrations/$($m.id)" -Headers $h).awaitingRevoke
Invoke-RestMethod "$lh/ca-migrations/$($m.id)/revoke" -Method Post -Headers $h -ContentType application/json -Body (@{ confirm = $n } | ConvertTo-Json)
# Move in from Keyfactor Command: Look (reads only), then Copy; the password is used for the request and never kept
$kf = @{ from = "keyfactor"; keyfactor = @{ url = "https://keyfactor.example.test/KeyfactorAPI"; authMethod = "basic"; user = "EXAMPLE\lh-move" }; secret = $env:KF_PASSWORD }
(Invoke-RestMethod "$lh/moves/look" -Method Post -Headers $h -ContentType application/json -Body ($kf | ConvertTo-Json -Depth 5)).lines | Select-Object area, name, status, becomes, reason
# Lines are ready, check (a real issue: something lost or not confirmed) or no (stays behind); list only what needs a person:
# (Invoke-RestMethod "$lh/moves/look" ...).lines | Where-Object status -ne "ready"
# Add includeKeys = $true to bring the keys Command holds (needs a key-encryption certificate here and Command's Recover permission;
# the move file's source.keysRecoverable says whether Command showed that permission on a role of the account)
$move = Invoke-RestMethod "$lh/moves" -Method Post -Headers $h -ContentType application/json -Body ($kf | ConvertTo-Json -Depth 5)
Invoke-RestMethod "$lh/moves/$($move.id)/undo" -Method Post -Headers $h -ContentType application/json -Body '{}'
# Key types: what certificates may have, then allow only RSA-3072, RSA-4096 and ECDSA-P256 with RSA-3072 made by default (administrators only)
Invoke-RestMethod "$lh/key-types" -Headers $h
Invoke-RestMethod "$lh/settings/key-types" -Method Put -Headers $h -ContentType application/json `
-Body '{"allowed":["RSA-3072","RSA-4096","ECDSA-P256"],"default":"RSA-3072"}'
# PFX downloads: keep PFX keys until 30 days after expiry (administrators only), then, as the person who enrolled it, get a PFX again
Invoke-RestMethod "$lh/settings/pfx-downloads" -Method Put -Headers $h -ContentType application/json -Body '{"keep":"after-expiry","for":30,"unit":"days"}'
$p = Invoke-RestMethod "$lh/certificates/$thumbprint/key/download" -Method Post -Headers $h
[IO.File]::WriteAllBytes("$PWD\$($p.fileName)", [Convert]::FromBase64String($p.pfx)); $p.password # shown once, kept nowhere
# Deprecated cryptography: what is refused, then allow it on one template for old devices (administrators only; a reason is required)
Invoke-RestMethod "$lh/crypto-policy" -Headers $h
Invoke-RestMethod "$lh/settings/crypto-policy" -Method Put -Headers $h -ContentType application/json `
-Body '{"everywhere":false,"templates":[12],"reason":"Floor 3 printers only do RSA-1024; replace by 2027-03"}'
# ...and back to refusing it everywhere
Invoke-RestMethod "$lh/settings/crypto-policy" -Method Put -Headers $h -ContentType application/json -Body '{"everywhere":false,"templates":[]}'
A CSR or certificate with deprecated cryptography (RSA under 2048 bits, DSA, curves under P-256, SHA-1, MD5 or MD2 signatures) is refused on every route that issues, with HTTP 400 and an error naming what was wrong, unless GET /crypto-policy shows it allowed for that template. See "Deprecated cryptography" in docs/server.md.
A certificate's common name is also one of its DNS names. POST /enroll/check answers with commonNameToAdd: the CN when it is a host name (with a dot) that the CSR does not also list as a DNS name, otherwise null. Lighthouse cannot change a signed CSR, so Vault and DigiCert templates ask the CA to add it and EJBCA and ADCS templates issue the CSR's names as they are; list the CN in the CSR to have it everywhere. A template's allowed domains apply to the CN too. See "Common name as a DNS name" in docs/server.md.
Nobody picks a key type when enrolling. A template's usableKeyTypes (from GET /templates) are the key types it takes once Settings, Application, Key types has narrowed it; a CSR with any other key is refused with 400. madeKeyType is the key to make when the key is made for the person, as the console does for a PFX. Templates take keyType (the key they make; null uses the Settings default) alongside allowedKeyTypes. See "Key types" in docs/server.md.
Facts: send values by fact name in facts for every id in the template's askedFacts (its own facts plus those with everyTemplate); a required one without a value or default answers 400 (required on GET /facts; Enroll tags each Fact Required or Optional). PUT /certificates/{thumbprint}/facts replaces all of a certificate's facts, so a script that fills in one should send the others back too; the rule not fact:<name> lists certificates without it (see the example above). Subject fields: the subject and effectiveSubject on GET /templates (and GET /subject-defaults) are what the console fills in when it builds a CSR in the browser; a CSR you send keeps its own subject. Templates take subject with the same fields; an empty field follows Settings. See "Facts" and "Certificate subject" in docs/server.md.
curl:
curl -H "Authorization: Bearer $LIGHTHOUSE_TOKEN" https://lighthouse.example.test/api/v1/summary
# Renew: a new CSR through a template; owners, tags and facts carry over
jq -n --arg csr "$(cat web01.csr)" '{templateId: 3, csr: $csr}' |
curl -H "Authorization: Bearer $LIGHTHOUSE_TOKEN" -H "Content-Type: application/json" -d @- \
"https://lighthouse.example.test/api/v1/certificates/$THUMBPRINT/renew"
What it covers
| Area | Routes |
|---|---|
| Inventory | GET /summary, /certificates (?expiring=, status, rule, sort, order, offset, limit; see Paging below), POST /certificates/query, GET /certificates/values, POST /certificates/served, /certificates.csv (?expiring=, status), /certificates/{thumbprint}, /pem, /history (see below), /authorities, /alerts/recent, /weak-rules, /crypto-policy, /key-types, /subject-defaults |
| Looking ahead | POST /adopt with {"address": "https://shop.example.com", "outpost": null} looks at one address like a scan of one port, records the certificate it serves and answers outcome (certificate, no-listener or no-certificate), the certificate (with its full subject, whose CN names a new Installation), the Installations serving it and suggestions of Installation kinds for the port (administrators only; audited as adopt). POST /adopt/server with {"host": "web01.example.com", "signInAs": "EXAMPLE\\lh-install-web", "signInPasswordSecret": "windows-web", "outpost": null} signs in to a Windows server and reads every https binding on its IIS sites without changing anything; it records each certificate with its place and answers bindings, each with site, port, hostHeader, the certificate, the name a new Installation would have (its CN), keptBy and a problem where none can be made (administrators only; audited as adopt-server; 502 when the server cannot be read, 409 when this server is not on Windows and no Outpost is given). GET /forecast?days=365&rule= lists what expires each day ahead (7 to 730 days) of the certificates you can see, with how many renew by themselves (renewsItself), wait for Run now (waitsForSomeone), the busiest weeks for people and expired. POST /rules/from-words with {"text": "wildcards expiring in 30 days", "ai": false} drafts a rule from a sentence on this server (rule, the parts understood, leftover words); with ai: true, only while Explain with AI is on and a model is set up, the model drafts it (from: "ai", audited as ai-rule). Nothing is saved. |
| Enroll, renew, revoke | POST /enroll/check, /enroll, /certificates/{thumbprint}/renew, /certificates/{thumbprint}/revoke, GET /templates, POST /templates, PUT/DELETE /templates/{id}, GET /enroll/key-encryption (the public key a browser-made key is sealed to; 404 when the server has no key-encryption certificate, so PFX downloads cannot be kept). Revoke works only on a certificate a CA Lighthouse manages issued (a CA a template connects to): on a self-signed one, or one from a CA Lighthouse only found, such as a vendor's own root, POST /certificates/{thumbprint}/revoke answers 409 with the reason and nothing waits for approval, and GET /certificates/{thumbprint} answers managed: false and revoke: null. Replace such a certificate with POST /certificates/{thumbprint}/renew through a template you pick. A template's renewWithoutApproval lets a renewal (/certificates/{thumbprint}/renew) skip its approval when it uses the same template, adds no names, is for a team (or person) that owns the old certificate and has a key at least as strong, and the old certificate is not revoked (renew-no-approval in the Keeper's Log); otherwise it answers 202 with the reason in the request's note. With wildcardsNeedApproval on (the default), a wildcard name (* in the common name or a DNS name) always answers 202, whatever the template, a Pipeline without an Approve step or Enroll without approval allow, unless the template has wildcardsWithoutApproval; ACME, SCEP, EST and lh enroll refuse it (wildcard-refused). |
| Who makes automation | Installations, ACME keys, SCEP challenges and EST credentials are for administrators, and for the people, groups and teams on GET/PUT /access/automation (Settings, People; PUT takes the whole list, [{"principal": "EXAMPLE\\alice", "until": "2026-12-31T23:59:59Z"}, {"team": 4}], and only administrators and people who make their own automation everywhere change it). Anyone else gets 403 on these routes. GET /me returns automates, automatesAll and automationTeams. Pipelines, Discovery jobs, Drills and CA migrations are for administrators only. |
| ACME keys | GET /acme-keys, POST /acme-keys ({"name", "team", "templateId"} for a team key, or {"name", "personal": true, "templateId"} for a personal key owned by you alone), DELETE /acme-keys/{id} (a personal key: its person or an administrator). Only the person and administrators see a personal key; it has "personal": true and person. An API token cannot make one. A personal key whose person lost access is suspended (suspendedAt, suspendedReason); administrators POST /acme-keys/{id}/resume or POST /acme-keys/{id}/give with {"team": 4}, and set the days without a sign-in with GET/PUT /settings/personal-acme-keys ({"inactiveDays": 90}). Its certificates show in Needs an owner, with lostOwner in /claims/suggestions. The ACME protocol itself is at /acme/<template id>/directory, see docs/server.md |
| SCEP challenges | GET /scep-challenges, POST /scep-challenges, DELETE /scep-challenges/{id} (the SCEP protocol itself is at /scep/<template id>, see docs/server.md) |
| EST credentials | GET /est-credentials, POST /est-credentials, DELETE /est-credentials/{id} (the EST protocol itself is at /.well-known/est/<template id>/, see docs/server.md) |
| Requests and claims | GET /requests, GET /requests/{id}, GET /requests/reject-reasons, POST /requests/{id}/approve, /reject, /cancel. Besides enroll, claim and revoke, kind may be access (a change that names its requester, with change in words and area, the Settings permission whose holders decide it) or recover (a key recovery waiting for a second person). A waiting request's expiresAt says when it is refused by itself; it then has status expired. An approved recover has expiresAt for the end of its download window and usedAt once used. GET /me returns decides, the kinds the caller decides wherever they come from; for administrators only (anyone else gets 403): POST /claims (give an owner), GET /claims/find, /claims/suggestions (what Needs an owner shows, the certificates nobody owns: each certificate's unownedDays, needsOwner from 30 days, requester, template, notOurs, and suggestions with a source such as same-name, requester, server or ai), POST /claims/not-ours with {"thumbprint": "...", "team": 4} (leave out team for yourself). Each waiting request also has canDecide (whether the caller may approve or reject it now) and, when not, whyNot (why, and who decides it instead); both are null once it is finished. Deciding your own request answers 403 with the same reason, unless PUT /settings/general with {"adminsApproveOwn": true} lets administrators decide their own (for testing; GET /me then returns approvesOwn: true for them). Rejecting takes {"reason": "Wrong template", "note": "use Web server"}, the reason one of GET /requests/reject-reasons. The approval email is GET/PUT /settings/approval-email and POST /settings/approval-email/preview (an administrator) |
| Owners and organizing | /owners, PUT /owners/{id}/access (a team's members, {"members": [{"principal": "EXAMPLE\\alice", "role": "lead"}, {"principal": "EXAMPLE\\bob", "role": "viewer", "until": "2026-12-31T23:59:59Z"}]}; roles viewer, requester, operator and lead; people who manage every team, or the team's leads up to operator), GET/POST /owners/{id}/tokens, POST /owners/{id}/tokens/{token}/rotate and DELETE /owners/{id}/tokens/{token} (the team's API tokens, each a member of the team: {"name": "web-deploy", "role": "requester", "days": 365}), PUT /owners/{id} (a lead changes only the email), PUT /owners/{id}/auto-renew, PUT /owners/{id}/change-window and GET /change-windows (a team's change window, such as sat,sun 02:00-04:00 in UTC; GET /change-windows/check?text= reads one without saving), /owners/{id}/summary, /teams, /locations, /spotlights (only the ones the caller may use; writes are for administrators), GET/PUT /spotlights/{id}/access and GET /spotlights/access (administrators: who may use each one), GET /spotlights/health, PUT/DELETE /spotlights/{id}/pin, GET /spotlights/fields for the fields the rule picker offers, /tags, PUT /certificates/{thumbprint}/owners, /tags, /facts, POST /certificates/bulk |
| Facts | /facts (a text fact may have a pattern, a regular expression the whole value must match; everyTemplate, true by default for a new fact, has every template ask for it when a person enrolls, and askedFacts on GET /templates lists what each template asks), POST /facts/pattern-test to try a pattern on sample values and, with factId, on the values certificates already have |
| Pipelines | GET /pipelines (events, the steps each may use, whether the caller may manage each, the default thresholds, and every Pipeline in order), POST /pipelines, PUT/DELETE /pipelines/{id}, PUT /pipelines/order with {"event": "certificate.requested", "ids": [3, 1, 2]} (an approve step takes days, 1 to 90, to refuse the request when nobody decides that round in time), POST /pipelines/try, GET /pipelines/runs (?thumbprint=); for administrators, and the alert events for people with an alerts permission (above). certificate.expiring and certificate.weak are the alerts: a Pipeline of theirs takes thresholds (expiring: "90,60,30,14,7,3,1,0", up to 20, 0 to 365; empty uses Settings, Application), repeatDays (expiring: 1 to 30, reminders after the last threshold until renewed or revoked), sendHour (0 to 23 UTC) and includeCaOnly; a tell step there takes together: true for one message per check, and to: "integrations" tells every integration that takes expiry alerts; any certificate.expiring step takes fromDays (0 to 365) to run only from that many days left. Wave 1: events request.waiting, request.approved, request.refused, facts.changed, spotlight.entered (with spotlightId), job.failed and key.recovered; steps call (webhook: a webhook id from /settings/webhooks, method POST, PUT or PATCH, optional body JSON with placeholders such as {certificate.name} and {reply.id}), ticket (to jira or servicenow, message, days to give up), approve on events that react (a request of kind pipeline), owner (team, change add, remove or replace; assign still reads as owner add), facts (fact, value), revoke (reason; needs the Pipeline's maxRevokes) and stop (message); every step takes when (a rule, plus reply:2xx, reply:none, failed:yes), and the ones that can fail onFail (continue or tell with failTo) and, for call, ticket and run, tries (2 to 5). A Pipeline of an event that reacts takes thenNext, maxRuns, maxInstalls and maxRevokes (1 to 10000 a day); a run past a limit is held. GET /pipelines also lists webhooks, spotlights, facts, reasons, placeholders, canFail and retried, and each Pipeline its version. POST /pipelines/test with {"pipeline": {...}, "id": 4, "thumbprint": "...", "tellTo": "[email protected]"} (what each step would do; nothing is sent but Tell steps to tellTo), POST /pipelines/replay with {"pipeline": {...}, "id": 4} (the last 30 days: events, runs, takenAbove, steps with count, examples, note), GET /pipelines/{id}/versions, POST /pipelines/{id}/versions/{version}/restore, POST /pipelines/runs/{id}/retry, /stop and /release (administrators; 409 when the run is not in a state for it), GET /pipelines/ready-made (each with key, questions and why it is suggested) and POST /pipelines/ready-made/{key} with {"answers": {"team": "4"}} (saves it off and answers each Pipeline with its replay) are for administrators. Wave 2: event schedule (with spotlightId, sendHour 0 to 23 and scheduleDays, such as "mon,thu"; empty is every day) runs for every certificate in the Spotlight, up to 2000; steps tighten (certificate.requested only: maxDays 1 to 3650, minKeySize 256 to 16384 as RSA strength, maxNames 1 to 100, noWildcards), ticket on request events (the request waits for it), ask (message, answers 2 to 4 or fact, to team, email:..., teams or slack, days 1 to 90; later steps use when: "answer:Not in use", answer:*, answer:none and {answer}) and served (after run; hours 1 to 168). GET /pipelines also lists overlaps (each Pipeline's hiddenBy and overrides), weekDays and drafts (true when Describe it can be used). GET /pipelines/export?ids=1,2 (a file with names in place of ids), POST /pipelines/import with {"file": {...}, "check": true} (answers saved and lines, all or nothing) and POST /pipelines/draft with {"description": "..."} (needs Explain with AI and an AI model; answers pipeline, switched off and not saved, problem and model) are for administrators. GET /answers (the open questions you may answer), GET /answers/{token} and POST /answers/{token} with {"answer": "Not in use"} are for the people asked and administrators; 409 once answered or past its time |
| Jobs | /jobs (writes: administrators, or a team's operators for its SSH Installations once they may make their own automation; Installations carry autoRenew, autoRenewApplied and renewalDue; kind jamf-read reads Mac certificates from Jamf Pro; kind renew-appliance, for administrators only and answering 404 while Network appliances is off, keeps an F5 BIG-IP, Palo Alto or Citrix ADC certificate current, see RenewApplianceParameters, renew-windows keeps a Windows service's certificate current (Remote Desktop, SQL Server, Exchange, AD FS, LDAPS, WinRM over https or the machine store, by use, see RenewWindowsParameters) and renew-windows-files keeps PFX or PEM files on Windows current (see RenewWindowsFilesParameters), both for administrators only, cloud-read reads a cloud vault and renew-cloud is an Installation in one; kind renew-kubernetes keeps a kubernetes.io/tls secret in a Kubernetes cluster current, see RenewKubernetesParameters; it answers 404 while Outposts is off and needs outpost, an Outpost running inside that cluster (400 without one), because the key is made there; an Installation saved without templateId only describes the place and reads what is there), POST /installations/check (the same body as POST /jobs, for an Installation kind only: runs a Practice run's read-only checks on it before it is saved and answers ready, checks and thumbprint; saves nothing, and needs the permissions to save it), POST /jobs/{id}/run, POST /jobs/{id}/install (gives an Installation the certificate to keep, like a certificate you can see or with typed names, and runs it; or with keptKey installs a certificate as it is with the key Lighthouse kept; or with keyFile (base64 PFX or PEM key file) and keyPassword installs it as it is with a key the caller brings, checked against the certificate, used once and never stored; keys go to files over SSH, IIS and the other Windows uses, and cloud vaults including Azure Key Vault, never through an Outpost; or with alreadyThere takes over the certificate already in place, after reading the place, without changing it, see InstallRequest), PUT /installation-secrets/{name} with {"value": "…"} (stores a password typed in an Installation's dialog under the name its signInPasswordSecret, pfxPasswordSecret, keystorePasswordSecret or passwordSecret gives; never replaces a stored secret no Installation uses; an Installation run from an Outpost seals it to the Outpost instead) Installations keep to their change window: they carry timing (own window, the one applied and where it is from, openNow, nextOpen, and any plan) and changeWindow in the request (left out, it stays as it is); POST /jobs/{id}/run on one outside its window needs an administrator and a reason in the body (409 with needsReason otherwise); an Installation whose own names include a wildcard that needs approval runs, is planned or is installed into only for someone who may approve requests for its team (403 otherwise), and its scheduled runs and Pipelines only tell people it is due; POST /jobs/{id}/install with a thumbprint may use a template that needs approval when it has renewWithoutApproval and the certificate was issued through it, is not revoked and is owned by the Installation's team; POST /jobs/{id}/plan with when (window, the default, now or a UTC time) and an optional change ticket, DELETE /jobs/{id}/plan cancels; the team is told each time. GET /certificates/{thumbprint}/installations lists the Installations that serve a certificate and POST /certificates/{thumbprint}/renew-installed gives each a new one, with the same when, ticket and reason. |
| Replace-All Drills | /drills, /drills/{id}, POST /drills/{id}/dry-run, /drills/{id}/replace (administrators only). POST /drills takes ca, spotlightId or both (what the CA issued that the Spotlight matches); each drill has scope in words. The console shows drills and CA migrations on one page, Replace many. |
| CA migration | /ca-migrations, /ca-migrations/{id}, POST /ca-migrations/{id}/check, /move, /revoke (with {"confirm": awaitingRevoke}), /undo (administrators only) |
| Move | GET /moves, /moves/{id}, POST /moves/look (reads only), POST /moves (Copy), POST /moves/file (a move file), POST /moves/{id}/teams/{teamId}/switch, /moves/{id}/finish, /moves/{id}/undo; needs an administrator |
| Kept keys | /keys, /certificates/{thumbprint}/key (DELETE destroys it), /key/download, /key/recover, /pfx-downloads. With a second person required (GET/PUT /access/safeguards, recoverySecondPerson), /key/recover first answers 202 with {"request", "waits"}; once someone else approves the recover request, the same call recovers the key once within recoveryHours |
| Reports | GET /reports (the reports, the teams and Spotlights the caller may pick, and the saved reports), GET /reports/{kind} and GET /reports/{kind}/csv with kind expiring, summary, ownership, crypto or access (Who has access: every grant, team member, shared Spotlight, template opened by name and API token; Settings, People only) and ?scope=all (default), team:4 or spotlight:7; Expiring soon also takes days= (1-3650, default 30), expired= (default true), group= (none, team, issuer); every report takes replaced=true to count certificates a newer one has replaced. A report is {title, scope, asOf, forecast, headline, tiles, tables, empty}; each table has columns, rows (strings) and links (each row's thumbprint or null). POST /reports/saved with {"name": "Monday expiring list", "kind": "expiring", "options": {"days": 14}, "scope": "team:4", "schedule": {"every": "weekly", "day": "monday", "at": "07:00", "to": "[email protected]", "skipIfEmpty": true}} (schedule null keeps it without email; every is daily, weekly or monthly with day 1-28), PUT/DELETE /reports/saved/{id}, POST /reports/saved/{id}/send (now, with the CSV attached) |
| Keeper's Log | GET /audit (administrators, Read the logs or Read the Keeper's Log): sign-ins, every change and every run, newest first; ?kind=sign-ins or settings, search=, take=, and before=<id> for the next page. POST /audit/check checks the seals |
| Rail headings | PUT /me/rail with {"collapsed": ["settings"]} (anyone signed in): which rail headings with places under them (spotlights, the pinned Spotlights under Certificates, and settings) you folded shut; [] opens them all. GET /me returns them as railCollapsed. Only you are affected |
| Dashboard layout | GET/PUT/DELETE /me/dashboard (anyone signed in: your own panels, tiles and order; DELETE goes back to the layout you start from, ?team= picks which of your teams' layouts that is), POST /me/dashboard/keep (keep your own layout after your starting layout changed), GET /me/dashboard/starting (what Reset goes back to, ?team= for one of your teams), GET /dashboard-layout (anyone signed in), PUT/DELETE /dashboard-layout (Settings, Server: the layout for everyone, DELETE puts the built-in one back), GET /dashboard-layout/teams (Settings, Server: every team's starting layout) and GET/PUT/DELETE /dashboard-layout/teams/{id} (the team's leads, and administrators through the API: the layout its members start from). PUT /me/next-steps with {"hidden": true} hides Suggested next steps for you (administrators). PUT /me/rail with {"collapsed": ["spotlights", "settings"]} keeps which rail headings you folded (anyone signed in; [] opens them all); GET /me returns them as railCollapsed. A layout is {"panels": [{"id": "runway", "shown": true, "width": "half", "pinned": false}, ...], "tiles": ["due", "spotlight:12"], "bars": null} with the panel ids steps, work, tiles, due, approvals, requests, found, runway, weak, spotlights, issuers, keys; width is half, full or null; tiles and bars (at most 12 each) are active, due, expired, quarter, unowned, kept, mine or spotlight:<id>, and bars: null shows the biggest 8. Your own tiles may name only Spotlights you can open; GET /me/dashboard leaves out the rest, lists the panels that apply to you as offered, and says startingChanged and dropped. |
| Optional features | GET /features (anyone signed in), PUT /settings/features/{key} with {"enabled": true} (an administrator); keys acme, device-enrollment (parts scep, est), appliances, replace-many (parts drills, ca-migration), move, macs, cloud-vaults, ai (parts mcp, ai-owner-suggestions, ai-explain), outposts, password-managers, ca-templates (parts ejbca-profiles, adcs-templates); a part's key switches its whole feature, and each feature lists its parts with their own enabled and licensed. webhooks, log-forwarding and public-cas have no switch. While one is off its routes answer 404. |
| License | GET /license and PUT /settings/license with {"text": "<the license file>"} (administrators only): state is trial, licensed, ending, grace or ended, with tier, until, graceUntil, certificates, inUse, features and newCertificates. GET /me tells everyone newCertificates and, when false, licenseMessage. Once new certificates have stopped, POST /enroll answers 403; renewals (POST /certificates/{thumbprint}/renew) always go through. A feature outside the license answers 404 saying which license tier has it, and PUT /settings/features/{key} answers 403 for it. |
| Banner | GET /banner (anyone signed in), PUT /settings/banner with {"enabled": true, "text": "TEST SERVER", "color": "amber", "bottom": false} (an administrator); color is blue, green, amber, red, purple, grey or #rrggbb, text one line of up to 200 characters. |
| Message for everyone signed in | GET /notice (anyone signed in; showing says whether the console shows it to you now), PUT /settings/notice with {"enabled": true, "title": "Planned maintenance", "text": "...", "kind": "warning", "from": null, "until": "2026-10-25T20:00:00Z", "dismissible": true} (an administrator); kind is info, warning or critical, title one line of up to 120 characters, text up to 4000 with line breaks. PUT /me/notice with {"dismissed": true} closes it for you until it is saved again. |
| Server log | GET /logs (?level=, source=, search=, from=, before=, take=; newest first), GET /logs/tail?after=<latest> for live tail, GET /logs/download (text, oldest first), GET /settings/server-log and PUT /settings/server-log with {"level": "Debug", "frameworkDetail": false, "retentionDays": 14} (an administrator, applies without a restart); the log itself needs See the server log |
| Cleanup (administrators only) | GET /settings/cleanup returns settings, wouldRemove (what each part would remove now with its days, on or not) and lastRun. PUT /settings/cleanup with {"expiredCertificates": true, "expiredDays": 90, "stalePlaces": false, "stalePlaceDays": 90, "deviceRequests": false, "deviceRequestDays": 30, "finishedRequests": false, "finishedRequestDays": 365} (every part off by default). POST /settings/cleanup/run runs the saved parts now (400 when none is on) and returns run and summary. The daily run is lighthouse-cleanup in the Keeper's Log; each removed certificate is a certificate-cleanup entry naming its thumbprint, each forgotten place place-cleanup. CA syncs leave removed certificates out. GET /settings/cleanup/kept-out lists the certificates an administrator deleted with keepOut (thumbprint, subject, serialNumber, notAfter, removedAt, removedBy, reason), newest first; DELETE /settings/cleanup/kept-out/{thumbprint} lets one back in (204, 404 when it is not kept out; certificate-let-back-in in the Keeper's Log). |
| Delete from Lighthouse (administrators only) | POST /certificates/delete with {"thumbprints": ["<sha-256>", ...], "keepOut": true, "reason": "test certificate"} (1 to 5,000). Each one goes with its places, owners, tags, Facts and sent-alert records, and is a certificate-delete entry in the Keeper's Log with what it was. It is not revoked. With keepOut no CA sync, scan, import, Outpost, Jamf or cloud read, Installation read or Move brings it back (their summaries count them as kept out); without it the next one that finds it adds it again. Returns deleted, keptOut, refused (thumbprint, subject, why: a kept private key or a waiting request holds it) and message. 403 for anyone but an administrator. |
| Password managers (administrators only) | Lighthouse reads secrets from CyberArk (Central Credential Provider), Delinea Secret Server, HashiCorp Vault (KV 1 or 2), Azure Key Vault, and any vault with a PowerShell SecretManagement extension (kind secretmanagement, with vaultName and no url), at the moment it needs them, and never keeps them. Anywhere a request names a stored secret (signInPasswordSecret, pfxPasswordSecret, keystorePasswordSecret, passwordSecret, apiKeySecret, credentialSecret, smtpPasswordSecret, an ACME CA's eabHmacSecret or dnsCredentialSecret), a reference may go instead: the manager's name, :, and where the secret is, such as cyberark:Windows/svc-install, cyberark:Windows/svc-install#UserName, delinea:1234/password, vault:lighthouse/smtp#password, azure:smtp-password or keepass:smtp-relay#UserName. GET /settings/password-managers answers managers (each manager, kindName, credentialSet, usedBy, pathExample), pointers, suggested and kinds. POST /settings/password-managers with {"manager": {"name": "cyberark", "kind": "cyberark", "url": "https://ccp.example.com", "appId": "Lighthouse"}} adds one (signIn defaults to the first way for the kind: windows, windows, approle, managed-identity, extension); "credential": "..." stores the password, token, AppRole secret id or client secret its sign-in needs in this server's secret store, never the database. PUT /settings/password-managers/{name} changes one (the name stays), DELETE removes one nothing reads from. POST /settings/password-managers/test with {"reference": "cyberark:Windows/svc-install"} reads it now and answers ok and message, never the value. PUT /settings/secret-pointers/{name} with {"reference": "cyberark:Mail/smtp-relay"} has one of Lighthouse's own fixed-name secrets (such as smtp-password, teams-webhook, jira-token) read from there; DELETE stops it. Every read, failed read and change is in the Keeper's Log without the value. A reference on a job that runs from an Outpost is read by the Outpost itself, which gets the manager's settings from /outpost/v1/password-managers/{name} with its own key. Needs the feature password-managers. |
| Outposts (administrators only) | GET /outposts; POST /outposts with {"name": "DMZ", "description": "Web servers behind the DMZ firewall"} returns outpost, server (this server's address, for the Outpost MSI's join screen), a one-time joinCode (lhj_..., works once within 24 hours), expiresAt, setupCommand (the msiexec line, with the Outpost MSI, to run on the Outpost's computer) and downloads; GET /outposts/downloads answers the same links for this server's version (setup, outpostMsi, serverMsi, checksums, in https://ironshoretech.com/downloads/lighthouse/<version>/); PUT /outposts/{id} with {"name": ..., "description": ..., "enabled": false}; POST /outposts/{id}/join-code makes a new code, for another computer, or to install one again (used on a computer of the same name it replaces that computer's key); DELETE /outposts/{id}/computers/{computerId} removes one computer and stops its key; DELETE /outposts/{id} (409 while scans or Installations run from it). Each Outpost shows joined, online (any of its computers called in within 2 minutes), machine, version and lastSeenAt of the computer that called in last, waiting, and computers, each with id, machine, version, keyHint, joinedAt, lastSeenAt, lastSeenFrom, online, doing, sealKey (the public half of the computer's sealing key, null on an older Outpost), sealKeyFingerprint, secrets (the names of the secrets it has, never values) and secretsWaiting. PUT /outposts/{id}/secrets/{name} with {"sealed": {"<computer id>": "v1...."}} sends a secret typed in the console, sealed in the browser to each computer's sealKey (RSA-OAEP SHA-256 wrapping an AES-256-GCM key, with iRonShore Lighthouse Outpost secret <name> as additional data; see OutpostSecretRequest in openapi.json); every computer with a sealKey must be given, Lighthouse cannot open the values and drops each once its computer takes it, and the answer names the computers it waits for (for) and the older ones it cannot reach (skipped). DELETE /outposts/{id}/secrets/{name} removes it from each computer on its next call. Set outpost on a scan (scan) or Installation (renew-iis, renew-windows, renew-windows-files, renew-ssh, renew-appliance) job to have that Outpost do its network part; only administrators set or change it. On POST/PUT /settings/cas for an adcs, ejbca or vault CA, "outpost": 3 has that Outpost read it and issue and revoke through it (0 is this server again, left out keeps what is set), and the CA answers outpost; a Vault apiKey sent with an Outpost is refused, since the credential is set on the Outpost (typed and sealed with PUT /outposts/{id}/secrets/{name}, or lh-outpost secret set there) under the CA's credential secret name. /templates answer outpost for a template whose CA is reached from one. Needs the feature outposts. The Outpost itself calls /outpost/v1 with its own key in X-Lighthouse-Outpost, never an API token; that protocol is not part of /api/v1 (see docs/server.md, Outposts). |
| Recipes | GET /recipes lists Recipes: everything for an administrator, only those with forTeams for a team operator who makes their own Installations (anyone else 403). Administrators only: GET /recipes/presets (nginx, Apache httpd, HAProxy, Tomcat, each with its fields and script); POST /recipes with {"name": "nginx web farm", "description": ..., "fields": [{"name": "certificatePath", "label": "Certificate file", "kind": "path", "required": true, "hint": "/etc/nginx/ssl/site.crt"}], "script": "lh_read() {...}", "forTeams": false}, or with "fromPreset": "nginx" and no script or fields to copy a preset; PUT /recipes/{id} (a change to script or fields makes a new version); DELETE /recipes/{id} (409 while Installations use it); POST /recipes/{id}/dry-run with {"host": "web01.example.com", "user": "lighthouse", "port": 22, "values": {...}} signs in, runs lh_read and lh_request, removes the key, and returns checks (each name, ok, detail), ready, version and fingerprint; a passing run marks that version tried. A Recipe has version, tried (this version passed), triedVersion, triedAt, triedBy, triedOn and usedBy. A renew-ssh job uses one with format: "recipe", recipeId and values (by field name; yes or no as "1" or "0", a secret field the name of a stored secret); it runs only a version that passed a Dry run, never from an Outpost, and for a team only a forTeams Recipe with installation- secrets. |
| SSH sign-in (administrators only) | GET /settings/ssh-sign-in answers Lighthouse's SSH CAs (userCa, hostCa public key lines and fingerprints, null until made), sshdLine, knownHostsLine, settings and the accepted hostKeys; PUT /settings/ssh-sign-in with {"certificateMinutes": 5, "hostCertificateDays": 365, "sourceAddresses": "10.0.5.20", "outpostAddresses": {"3": "192.168.50.10"}, "ownHostCaPattern": "*", "hostCas": [{"pattern": "*.example.com", "key": "ssh-ed25519 AAAA..."}]}; POST /settings/ssh-sign-in/ca makes whichever CA is missing (the private keys go to the secret store as ssh-user-ca and ssh-host-ca); DELETE /settings/ssh-sign-in/host-keys?host=web01&port=22&fingerprint=SHA256:.... On a renew-ssh Installation: GET /jobs/{id}/ssh/host-keys asks the server (or its Outpost) for its host keys, each with fingerprint and accepted, and changed; POST /jobs/{id}/ssh/host-keys with {"fingerprints": [...]} accepts only keys the server still offers with those fingerprints; POST /jobs/{id}/ssh/switch-over with {"hostCertificates": false} makes the server trust Lighthouse's CA through sudo -n (checked with sshd -t, undone on failure), proves a certificate sign-in and only then sets the Installation's signIn to certificate, answering proven, steps and authorizedKeys; GET /jobs/{id}/ssh/authorized-keys and POST /jobs/{id}/ssh/remove-keys with {"fingerprints": [...]} list and remove old keys, signed in with a certificate. A renew-ssh job's signIn is server-key (default), certificate or stored-key with sshKeySecret. |
| Settings and access (by group, see above) | /settings (its siteName is the site name in use), PUT /settings/site-name with {"name": "Acme PKI"} ( null or "" goes back to Lighthouse; GET /me also returns siteName, and copyright, the copyright line the console shows, from 2026 to the server's current year), PUT /settings/weak-rules, PUT /settings/key-types ({"allowed": [...], "default": "RSA-3072"}), PUT /settings/subject-defaults ({"organization": "Example Inc.", "organizationalUnit": "IT", "locality": "Seattle", "state": "Washington", "country": "US", "email": "[email protected]"}, all optional), PUT /settings/pfx-downloads ({"keep": "before-expiry", "for": 30, "unit": "days"}), GET /settings/key-encryption (the key-encryption certificate, where its key lives, and its recovery copy), POST /settings/key-encryption/recovery-copy with {"parts": 1} for one passphrase or {"parts": 5, "needed": 3} to split it (answers the passphrase or parts in secrets, only this once), POST /settings/key-encryption/recovery-copy/check with {"secrets": ["..."]}, GET /settings/key-encryption/recovery-copy/file (the locked PFX) and DELETE /settings/key-encryption/recovery-copy, PUT /settings/crypto-policy, GET/PUT /settings/ai-suggestions ({"maskNames": true}, the default, masks names for AI owner suggestions), /settings/cas (kinds adcs, ejbca, digicert, vault, and with the feature public-cas on acme, sectigo, godaddy, see "More public CAs" below; POST /settings/cas/test, /settings/cas/{id}/test, GET /settings/cas/{id}/certificate-templates for an ADCS CA's templates, GET /settings/cas/{id}/ejbca-profiles for an EJBCA CA's CAs and profiles; an ADCS or EJBCA CA's settings also take Bring in, "onlyTemplates": ["WebServer", "LH-Web"] (ADCS template names or OIDs, or EJBCA certificate profiles; null brings in every template) and "skipExpired": true; GET /settings/cas/{id}/sync-templates lists the templates to pick from (name, certificates in Lighthouse, atCa, and a note when the CA's own list could not be read), GET /settings/cas/{id}/left-out counts the certificates already in Lighthouse that the setting leaves out (certificates, removable, kept, and readFirst until a read has used the current setting), and POST /settings/cas/{id}/left-out/remove removes the removable ones (at most 10,000 per call, more when some are left); with the feature ejbca-profiles on, POST /settings/cas/{id}/ejbca-profiles/preview and POST /settings/cas/{id}/ejbca-profiles with {"name": "LH Web 1y", "startFrom": "WebServer EE", "cas": ["Issuing CA"], "dnsNames": 10, "dnsRequired": 1, "ipAddresses": 0, "emails": 0, "upns": 0, "validityDays": 365} make a new end entity profile copied from startFrom, through EJBCA Enterprise's REST ConfigDump with the CA's profileCertificateThumbprint, or answer direct: false with steps for EJBCA's admin web; with the same feature on, GET /settings/cas/{id}/ejbca-autoenrollment lists the forest's templates to start from, the EJBCA CA's template account setup (delegation, two rights), its alias, policyServerUrl and the one-time setup, and POST /settings/cas/{id}/ejbca-autoenrollment/preview and POST /settings/cas/{id}/ejbca-autoenrollment with {"name": "LH-Computer-EJBCA", "startFrom": "Computer", "uses": ["client"], "directoryNames": ["dns"], "enroll": ["CONTOSO\\Domain Computers"], "endEntityProfileFrom": "Computer EE", "cas": ["Issuing CA"]} make a Windows autoenrollment template in Active Directory (AutoEnroll for enroll, names from Active Directory, published on no ADCS CA) with the EJBCA CA's templateAccount, then its end entity and certificate profiles with the profileCertificateThumbprint, taking the template away again if EJBCA refuses, and answer the steps left in EJBCA's admin web and Group Policy; see docs/ejbca-autoenrollment.md; with the feature adcs-templates on, GET /settings/cas/{id}/adcs-templates lists an ADCS CA's forest templates to start from, its template account's setup script (delegation) and whether that account has each of its three rights (rights, each given true, false or null when unreadable), and POST /settings/cas/{id}/adcs-templates/preview and POST /settings/cas/{id}/adcs-templates with {"name": "LH-Web-1y", "startFrom": "WebServer", "uses": ["server"], "namesFrom": "request", "validityDays": 365, "minimumKeySize": 3072, "enroll": ["CONTOSO\\Web Admins"], "publish": true} make a new certificate template copied from startFrom with the CA's templateAccount, or answer direct: false with a PowerShell script; see docs/adcs-templates.md. Both creates also add the Lighthouse template that issues with what they made, named after the profile or the template's display name and open to administrators only, unless "lighthouse" says otherwise: {"name": "Web servers 1y", "teams": [3], "people": ["CONTOSO\\web-admins"]}, or {"add": false} for none. Preview answers the name in lighthouseTemplate, create adds lighthouseTemplateId, and lighthouseTemplateNote says why it was not added (such as a name in use) while the CA side stays made), /settings/team-summaries (POST /settings/team-summaries/send), /settings/webhooks (POST /settings/webhooks/{id}/test, /secret), /settings/log-destinations (POST /settings/log-destinations/{id}/test), PUT /settings/mail and POST /settings/mail/test (email), GET /settings/message-wording (each automatic message's wording, defaults, custom, placeholders and itemPlaceholders), PUT /settings/message-wording/{kind} with {"subject": "[PKI] {summary}", "body": "...{certificates}...", "item": "- {shortName} {state}", "ticketTitle": "Renew {shortName}"} (a part left null keeps its default), DELETE /settings/message-wording/{kind} (back to the defaults) and POST /settings/message-wording/{kind}/preview (example certificates, nothing sent); kind is expiry, weak, changes, renewal-due, job-failed or team-summary, PUT /settings/general (publicUrl, thresholds, includeCaOnly, autoRenew, adminsApproveOwn, wildcardsNeedApproval, adminsIntendedOwner; GET /me also returns wildcardsNeedApproval and adminsIntendedOwner, which stops the Dashboard asking for owners), PUT/DELETE /settings/integrations/{kind} and POST /settings/integrations/{kind}/test (Teams, Slack, Jira, ServiceNow), GET/PUT /settings/signin and PUT/DELETE /settings/signin/providers/{id}, POST /settings/signin/providers/{id}/test (see docs/sign-in-providers.md), GET/PUT /settings/sessions ({"idleMinutes": 30, "longestHours": 12, "askAgain": false}: minutes without use 5 to 480, longest session 1 to 24 hours, and whether providers ask for the password again after a timeout or Sign out), /server, GET /servers and DELETE /servers/{name} (servers sharing the database), /access/profiles, /access/grants, /access/people, GET /access/permissions, GET /access/install, GET/PUT /access/new-people ({"profileId": 2, "ownOnly": true}) (a person or group has one grant of each profile, whatever the case of the name: POST /access/grants for a profile they already have adds to that grant, everywhere covering any teams), /access/tokens (POST /access/tokens/{id}/rotate gives a new secret; "team": 4 on POST has that team look after the token: its email and the token's maker are told 30 and 7 days before it expires), POST /access/check with {"principal": "EXAMPLE\\alice", "groups": ["EXAMPLE\\Web Devs"]} (what someone may do and every reason why: administrator, areas, everywhere, teams, spotlights, templates, automates, sources), GET /certificates/{thumbprint}/who (who sees one certificate and how), GET/PUT /access/safeguards ({"recoverySecondPerson": false, "recoveryHours": 4, "stillNeeded": true, "stillNeededTo": null, "unusedDays": 90}; only an administrator changes the first two) and GET /access/still-needed (this month's Still needed? list), GET /templates/{id}/adcs-enroll (Settings, Connections: who has Enroll on an ADCS template in Active Directory compared with the template's Who may enroll) |
Holding keys: a template's keepKeyUnit is days, weeks, months, years, indefinitely, never, or null (follow Settings, PFX downloads, which holds nothing until an administrator turns it on, so older templates behave as before), and keepKeyFor is the number (1-3650 days, 1-520 weeks, 1-120 months, 1-10 years; null for indefinitely and never). holdsKey and keyHoldText on GET /templates say what applies; send the sealed key with POST /enroll only when holdsKey is true. The older keepKeyDays still works on requests when keepKeyUnit is not sent, and on answers it is the hold in whole days for days and weeks, otherwise null. A kept key's deleteAfter is null when it is held indefinitely.
Paging: GET /certificates with no new parameters lists every certificate the caller sees, as it always has. With offset or limit (0 to 2000; 100 when only offset is given) it answers one page, and the header X-Total-Count says how many match in all. rule narrows the list with a Spotlight rule (such as expires<=30 and owner:none), sort is daysLeft (default), notAfter, name or issuer, and order is asc or desc; certificates that sort the same are ordered by thumbprint, so pages never repeat or skip one. POST /certificates/query is what the console uses: {rule, search, sort, order, offset, limit, counts, breakdown, horizon, thumbprints, status} answers {all, total, attention, keysKept, offset, limit, certificates, counts, status, hidden}, where counts counts up to 300 other rules over everything the caller sees (a rule that is not understood has n null and an error), breakdown adds issuers and keyTypes (the six commonest and Other) and owners, horizon adds the soonest to expire, and thumbprints every matching thumbprint (up to 10,000). GET /certificates/values?field=domain suggests values for a rule field, most common first.
Managed CAs: a certificate is issued by a managed CA when a template connects to the CA it was read from, or to the CA its issuer names (an EJBCA template's CA, an ADCS template's CA, or DigiCert for a DigiCert template). GET /certificates/{thumbprint} answers managed (true or false), and the rule term managed:no (or managed:yes) finds them in POST /certificates/query, GET /certificates?rule=, Spotlights and the MCP find_certificates tool. managed:no is the built-in Self-signed Spotlight on Certificates: self-signed certificates and those from a CA Lighthouse only found, such as a vendor's own root. Lighthouse cannot revoke them (POST /certificates/{thumbprint}/revoke answers 409 with error), only replace them with POST /certificates/{thumbprint}/renew (or /renew-installed where Installations serve them), so the new one comes from a managed CA. When a template comes to connect to a certificate's CA, the certificate counts as managed from then on.
Status: status on GET /certificates, /certificates.csv and POST /certificates/query is active (in date and not revoked), expired (past its end date and not revoked), revoked or all. Revoked wins, so a revoked certificate is never expired, and the rule term state:expired means the same. Left out, status is all, so a script written before it sees what it always did; the console always sends active unless someone picks another. On the query the status narrows the rule, the search, every count and the breakdown, except a rule (or count) that names a status itself with state:active, state:expired or state:revoked; the answer's status is the one applied (null when the rule named one) and hidden how many more match the rule and search with another status. attention and the horizon never count a revoked certificate. GET /summary counts revoked certificates in revoked and leaves them out of expired, expiringWithin30Days and expiringWithin90Days. The CSV has a Status column. Reports and certificate history are unchanged: they include every status. POST /certificates/served with {lookups: [{file}, {where: "web01.example.com:443"}]} finds the certificate seen at each place, or null.
Teams: GET /owners and GET /teams list teams only. A person who owns certificates themselves (the owner named after their account that enrolling or a personal ACME key makes) is not a team, so it is left out there and from the teams GET /reports offers, though it still shows among a certificate's owners. POST /owners with that person's name is refused with 400, and PUT /owners/{id}/access on it answers 404.
Owners: GET /certificates/{thumbprint} lists owners (given by hand) and ownedThrough, the team Spotlights that make a team its owner, each {teamId, team, spotlightId, spotlight}; both count the same. When neither gives it an owner, defaultOwner is "Admins": the administrators own it by default and it is in the built-in Needs an owner Spotlight (rule owner:none) until someone gives it an owner; otherwise it is null. PUT /certificates/{thumbprint}/owners with {"owners": [4], "note": "they run the site now"} sets the owners given by hand and never changes a Spotlight; note (optional, up to 1000 characters) goes with the new owners' names into the history and the Keeper's Log. To take a certificate away from a team that owns it through a Spotlight, change that Spotlight's rule.
Certificate history: GET /certificates/{thumbprint}/history answers {thumbprint, previous, next, latest, events}. events run newest first, each with when, kind (issued, enrolled, found, seen, installed, removed, renewed, replaced, owners, tags, facts, key, request, pipeline, revoked, expired, expires or other), title, detail, who, thumbprint (another version it links to) and upcoming (true for an expiry still ahead). previous, next and latest are the versions before, after and newest, each with thumbprint, subject, notBefore, notAfter, revoked and how: renewal when Lighthouse renewed it, names when a newer certificate that is not revoked covers all its names. It is built from the inventory and the Keeper's Log entries that name the certificate when asked; links to certificates the caller may not see are left out, and a certificate the caller may not see answers 404.
PFX passwords: the console makes the password of a PFX made in the browser and shows it once; Lighthouse never sees it. GET /pfx-downloads says how long the key is held so the PFX can be downloaded again: keep is off (default), indefinitely, before-expiry or after-expiry, with for and unit (days, weeks, months) for the last two. POST /certificates/{thumbprint}/key/download is for the person who enrolled it (canDownloadAgain on GET /certificates/{thumbprint}/key) and answers {fileName, pfx (base64), password, deleteAfter} with a new password each time, audited as pfx-download. POST /certificates/{thumbprint}/key/recover with only a reason answers the same JSON with a password Lighthouse made; with a password of your own it still answers the PKCS#12 file. If a certificate expires before a before-expiry time, no key is kept and POST /enroll says why in keyNote.
More public CAs (part public-cas, always there within the license): a CA in POST /settings/cas may be "kind": "acme" with settings {"directoryUrl": "https://acme.zerossl.com/v2/DV90", "email": "[email protected]", "eabKeyId": "...", "challenge": "dns-cloudflare", "waitSeconds": 60} and the secrets eabHmacKey and dnsToken beside them (stored, never returned; challenge is dns-cloudflare, dns-manual, http-01 or prevalidated). An ACME CA has no read job, since ACME cannot list what it issued. "kind": "sectigo" takes {"customerUri": "acme", "login": "api-user"} and the SCM password as apiKey; "kind": "godaddy" takes {"customerId": "<GUID>"} and key:secret as apiKey; both get a read job (sectigo-sync, godaddy-sync). GET /settings/cas/acme-directories lists known ACME CAs (name, url, binding, note); GET /settings/cas/{id}/sectigo-profiles?organizationId= lists a Sectigo CA's SSL profiles. Templates take "kind": "acme" with acme (the same fields as the CA, plus authorityName), acmeEabHmacKey and acmeDnsToken; "kind": "sectigo" with sectigo (customerUri, login, organizationId, certType, term, passwordSecret, url, waitSeconds) and sectigoPassword; "kind": "godaddy" with goDaddy (contactEmail, contactFirstName, contactLastName, contactPhone, contactOrganization, productType, period, customerId, apiKeySecret, url, waitSeconds) and goDaddyApiKey. An order the CA has not issued yet answers 409 with what to do; with dns-manual that is the TXT records to add, and sending the same request again finishes the same order. Lighthouse answers the CA's http-01 checks at GET /.well-known/acme-challenge/{token} (no sign-in). See "More public CAs" in docs/server.md.
GET /templates is the list the console's Enroll page picks from: people choose a template there, then send its id as templateId to POST /enroll. An administrator gets every template (including ones that are off); anyone else only the templates opened to them: those whose people name them or a group of theirs (they enroll for themselves, team left out), or whose teams list a team where they hold Enroll certificates for the team itself (they enroll for that team). Enroll certificates given everywhere opens no template, and an API token is named as token:<name>. Templates take "people": ["EXAMPLE\\alice", "group:pki-requesters"] alongside teams; people is shown to administrators only. "peopleUntil": {"EXAMPLE\\alice": "2026-12-31T23:59:59Z"} ends some of them on a date, "teamNamesOnly": true lets a team's request name only what its team Spotlights cover, and "requestsExpireDays": 14 refuses a request that waits for approval longer than that (1 to 90). GET /me returns enrollsFor, the ids of the templates opened to the caller by name or group. See "Who sees which templates" in docs/server.md.
In the body of POST /templates or PUT /templates/{id}: "keepKeyUnit": "years", "keepKeyFor": 2 holds keys for two years, "keepKeyUnit": "indefinitely" until someone destroys them, "keepKeyUnit": "never" not at all, and "keepKeyUnit": null as Settings, PFX downloads says.
Change watch: a scan job's parameters take watchChanges (default true), and each integration under /settings has changes (default true) for whether it gets site change notices. The changes a scan found are in /audit?search=site-change.
Every optional feature (keys above) but ACME is off on a new server, including Network appliances, Cloud vaults, Macs and AI. Routes of their own (/acme-keys, /scep-challenges, /est-credentials, /drills, /ca-migrations, /moves, /outposts, /settings/password-managers, and the ACME, SCEP, EST and Outpost protocols under /acme/, /scep/, /.well-known/est/ and /outpost/v1/) answer 404 with an error naming Settings, Features until an administrator turns them on, so check GET /features first if a script gets an unexpected 404.
To be told when something happens instead of asking, add a webhook (Settings, Integrations, or POST /settings/webhooks) and call it from a Pipeline of the events you want with a call step. Lighthouse then posts signed JSON events to your URL; see Webhooks in docs/server.md for the events, the body and how to check the signature. A webhook takes no events any more (400 when given), GET /settings/webhooks answers calledBy, the Pipelines that call each one, and DELETE /settings/webhooks/{id} answers 409 while a Pipeline calls it.
To keep Lighthouse's server log, audit log and alerts in Splunk, a syslog receiver or another log service, add a log destination (POST /settings/log-destinations with kind splunk, syslog or https); see Log forwarding in docs/server.md for the settings and the event format. For example:
$body = @{ name = "Splunk"; kind = "splunk"; url = "https://splunk.example.test:8088"; token = $env:HEC_TOKEN; index = "pki"; streams = @("audit", "alerts", "app"); minimumLevel = "Warning" } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri https://lighthouse.example.test/api/v1/settings/log-destinations -Headers @{ Authorization = "Bearer $env:LIGHTHOUSE_TOKEN" } -ContentType application/json -Body $body
Pipelines decide what happens when certificates are requested, issued, renewed, revoked, expiring, weak or found, change owner or fail to install; see Pipelines in docs/server.md. A request a Pipeline holds answers 202 like any waiting request, with pipeline, waiting and approvals on it; one a Pipeline refuses answers 400 with the Pipeline's message. For example, two Payments approvers and then one from Security for PCI names:
$body = @{ name = "PCI"; event = "certificate.requested"; rule = "domain:pci.example.com";
steps = @(@{ kind = "approve"; count = 2; team = 4 }, @{ kind = "approve"; count = 1; team = 7 }, @{ kind = "tell"; to = "teams" }) } | ConvertTo-Json -Depth 4
Invoke-RestMethod "$lh/pipelines" -Method Post -Headers $h -ContentType application/json -Body $body
# Which Pipeline would a request for pay.pci.example.com through template 3 take?
Invoke-RestMethod "$lh/pipelines/try" -Method Post -Headers $h -ContentType application/json -Body '{"event":"certificate.requested","names":"pay.pci.example.com","templateId":3}'
Alerts are Pipelines too. Payments hears at 90, 60, 30, 14, 7, 3 and 1 days and then every day, in one list per morning check at 07:00 UTC; its lead is told from 7 days left and ServiceNow gets a ticket from 3:
$body = @{ name = "Payments, escalating"; event = "certificate.expiring"; rule = "team:Payments"; thresholds = "90,60,30,14,7,3,1"; repeatDays = 1; sendHour = 7
steps = @(@{ kind = "tell"; to = "team"; together = $true }, @{ kind = "tell"; to = "email:[email protected]"; together = $true; fromDays = 7 },
@{ kind = "tell"; to = "servicenow"; fromDays = 3 }) } | ConvertTo-Json -Depth 4
Invoke-RestMethod "$lh/pipelines" -Method Post -Headers $h -ContentType application/json -Body $body
AI assistants (MCP)
AI assistants such as Claude, Copilot or Cursor can read Lighthouse through the Model Context Protocol at /mcp, beside the API rather than under /api/v1. It is off until an administrator turns on AI under Settings, Features. It takes only an API token (Authorization: Bearer lh_...), never a browser session, and a token sees by MCP exactly what its grant lets it see at /api/v1. Every tool only reads, except suggest_owner (below), which only leaves a suggestion for a person; nothing is changed, enrolled or revoked. Lighthouse itself sends nothing to any AI service: the assistant asks, and whoever runs the assistant decides where its model runs.
Give the token a read-only grant (built-in profile Read-only, limited to teams if you like) and add the server to the assistant. For Claude Code:
claude mcp add --transport http lighthouse https://lighthouse.example.test/mcp \
--header "Authorization: Bearer $LIGHTHOUSE_TOKEN"
Other assistants take the same address and header in their MCP settings, as a "streamable HTTP" server.
| Tool | What it answers |
|---|---|
summary |
The counts on the Dashboard: total, expired, expiring within 30 and 90 days, where found, last scan and sync |
find_certificates |
Certificates matching a Spotlight rule (rule, such as expires<=30 and owner:none) and/or a saved Spotlight (spotlight), soonest to expire first; limit 1 to 200 (50), total counts all matches; status is active (the default), expired, revoked or all, unless the rule names a state: |
get_certificate |
One certificate by SHA-256 thumbprint: names, key, dates, where seen, owners, facts, tags, weak, revoked (no PEM, no owner email addresses) |
list_spotlights, list_teams, list_cas, list_jobs |
The saved Spotlights the token may use and their rules, teams with their certificate counts, CAs, and jobs with their last result |
recent_alerts |
Expiry alerts sent most recently (limit 1 to 100, 25) |
list_unowned |
Only while AI owner suggestions are on, and for an administrator's token: the unowned certificates, unowned longest first, with Lighthouse's suggestions, plus each team's id and a few names it owns; limit 1 to 200 (50), needs_owner for 30+ days only. Names are masked unless an administrator turned that off |
suggest_owner |
Only while AI owner suggestions are on: thumbprint, team (id) and reason (one line, at most 300 characters). Records a suggestion that shows in Needs an owner for an administrator to give or say not ours; nothing changes hands. An administrator's token only. Audited as owner-suggest-ai. The one tool that writes, so it is not marked read-only |
Explain with AI
With the feature ai-explain on and an AI model set up, POST /explain asks the customer's own model to explain one item and answers with its explanation. Lighthouse calls no AI service of its own; see Running the server for what is sent and what is hidden.
| Call | What |
|---|---|
GET /explain |
Whether Explain can be used (ready) and which model; anyone signed in |
POST /explain |
{"kind": "job", "id": "12"}: kind job (also Installations), request, certificate (id is the SHA-256 thumbprint), log (id is the line's seq, with at, its time) or audit. Answers title, answer, sent (exactly what went to the model), hiddenNames and model. "preview": true answers sent only and sends nothing. 409 when no model is set up, 404 for an item the caller may not see, 502 when the model cannot be reached or refuses |
GET /settings/ai-model, PUT /settings/ai-model |
The model (administrators): provider (openai, azure-openai or anthropic), url, model, maskNames and, when you change it, key (or clearKey). The key is kept in the secret store; reads only say keySet |
POST /settings/ai-model/test |
Sends a test question with nothing from Lighthouse in it; answers ok and message |
Each question is audited as ai-explain. The model only answers: nothing is changed.
Invoke-RestMethod "$lh/explain" -Method Post -Headers $h -ContentType application/json -Body '{"kind":"job","id":"12"}' | Select-Object -ExpandProperty answer
The endpoint is JSON-RPC 2.0 over one POST per message (protocol versions 2025-11-25, 2025-06-18 and 2025-03-26), answered with JSON; GET answers 405 because nothing is pushed back. A request with an Origin header from another site is refused.
Changing the API
Every route under /api/v1 must be described in src/Lighthouse.Server/Dashboard/openapi.json; a test fails otherwise. Add new routes and new optional fields freely. Do not remove a route, rename a field or change what one means: start /api/v2 beside it instead.