Skip to content

Kuiper API Reference

Kuiper exposes a REST API that can perform nearly all configuration and deployment actions available in the UI. This page records what the API surface actually is — both the endpoints Epic documents in the Kuiper API Specifications PDF and the undocumented ones discovered by inspecting a live install — so the kuiper role's API configuration step has an authoritative map to build against.

Authentication

POST /Kuiper/api/auth/v1 with a JSON body:

{
  "username": "DOMAIN\\user",
  "password": "…",
  "grant_type": "password",
  "client_id": "CLIENTIDNULL"
}

Returns a JWT (token, token_type, expires_in — currently a 7-day expiry). Send it on every subsequent call as Authorization: Bearer <token>.

Notes learned in the Azure IRE environment:

  • The username must be the account's sAMAccountName, which AD truncates to 20 characters. The UPN form (user@domain) and the untruncated display name both return 401 Unauthorized with an empty body.
  • MFA-enrolled users must supply a passcode (TOTP). Duo Web SDK / OIDC users cannot authenticate through the API at all.
  • Prefer a dedicated, least-privilege Kuiper user for automation rather than a domain admin.

Two coexisting routing schemes

A live install serves two route conventions, both accepting the same bearer token:

  1. Public API — resource-first, version-suffixed: /Kuiper/api/{resource}/v{n} (e.g. /Kuiper/api/machines/v2, /Kuiper/api/channels/v1). This is what the Epic PDF documents and what automation should target.
  2. Internal / UI API — version-first: /Kuiper/api/v1/{resource} (e.g. /Kuiper/api/v1/servers, /Kuiper/api/v1/settings/*). This is what the Kuiper web UI itself calls. It is broader and more granular but undocumented and unsupported — treat it as a last resort and expect it to change between versions.

Newer controllers use ASP.NET API-versioning and are strict about method + version; calling the wrong method returns 405 UnsupportedApiVersion (which is still a useful "this endpoint exists" signal).

How the surface was discovered

Kuiper ships no Swagger/OpenAPI document (swagger.json / openapi.json return 404; the SPA catch-all returns the app shell for unknown paths). The authoritative source is therefore the deployed application itself, at C:\Epic\ECSM Tools\Web:

  • String-scan bin\*.dll for api/… route literals and *ApiController class names. The *ApiController classes are the public API surface.
  • Confirm candidates with read-only GET probes on the host (https://localhost/Kuiper/…). 200/400/405/415 mean the route exists; 404 with a "No type was found that matches the controller" body means that path is not it.

Documented endpoint families (Epic PDF)

auth, actions, applicationinventory, auditlog, backgroundprocesses, channels (+ Hyperdrive package authorization/activation), clientregistrationkey, credentials, deploymentextensions, directorygroups (retired May 2026 → mapgroups), domains, environments, eki, epicwebview2, eventmessages, groups (+ grouptags, group membership, per-group credential), Kuiper Cache Service health, Hyperdrive filelog, ldapconnection, machines (v2/v3), mapgroups, mission, packages, printers (print queue), servicegroup, settings/* (deployment, devices, environments, extensibility, filemanagement, hyperdrive-registration-only, infrastructuredeployment (new May 2026: enableNtlm — the "Remote authentication" toggle — and maximumConcurrentActions; GET/PATCH with an {"attributes": {...}} body), machinecleanup, mfaprovider, mychart, nebula, packages/packagesources, reauthentication, satellite, su-eeds, systempulse), thirdpartyfilepaths, upgradeconsole, validation.

Undocumented endpoints (confirmed live)

Discovered by DLL scrape and confirmed against the running server; not in the Epic PDF. Routes/behaviour may change without notice.

Endpoint Method(s) Notes
api/settings/v1/satellite/publish POST (200) Publishes the Satellite build shipped with the Kuiper install to production machines — the REST equivalent of the UI's "Publish " button (which itself POSTs to the MVC action Settings/PublishSatelliteToProduction). Requires a body ({} is fine; a body-less POST gets HTTP 411) and returns the updated SatelliteSettings, whose satelliteVersion goes from null to the published version. No published-version staleness info is exposed, so idempotency is limited to "publish only while satelliteVersion is null".
api/thirdpartysources/v1 GET (200) Third-party download sources; distinct from settings/packagesources and thirdpartyfilepaths.
api/settings/v1/packagesources/{id} PATCH (200) Update one package source, JSON:API {attributes: {...}} body (a flat body 400s UnknownKeyInPrimaryDataObject; PUT, and collection-level PATCH/PUT, all 405). The built-in Base Package Source (Kuiper's default package-staging location, sourceType: "Base Package Source", id 1 on a fresh install) is repointed this way — see Base Package Source. Two gotchas: changing userName without a non-blank password 409s ("Changing the user name without also updating the password is not allowed"), so a passwordless MSA can't be set here (and isMsa is not a valid attribute); and maxConcurrentDownloads must be 1–200 on write even though a fresh source reads 0.
api/cloudservice/v1 write (GET→405) Cloud service token management; pairs with settings/EnableCloudServiceTokenManagement.
api/v1/servers GET (200) Server records — richer than public machines (associatedApplications, aliases). Accepts a ?groupId={id} filter that returns only that group's members — the read-back for group membership (see below).
api/groups/v1/{application} GET (200) / POST (200/201) Application groups. Create body is {attributes: {name, systemPulseMetricGroup, deploymentSetting, ...}}. systemPulseMetricGroup is the group's Metric group — a friendly application-name string (Hyperspace Web, Interconnect, System Pulse, default Disabled); the values map to I_DistributedMetricDefinitions rows. A deploymentSetting key is required (HyperspaceWeb needs a populated one; Satellite Only — the System Pulse monitoring app — accepts an empty {} and Kuiper fills defaults). Every application uses this route, Interconnect included, from Kuiper August 2026 onward — earlier builds returned 400 "Creating a Interconnect application group is currently not supported" and needed the MVC path. For an Interconnect group the server expands deploymentSetting.instances into one secondary application group per entry and returns them under relationships.instances, so no SecondaryApplicationGroups array and no DistributedMetricDefinitionId are sent.
api/groups/v1/{application}/{groupId} PATCH (200) Update a group. The body must carry the group's full attributes (including deploymentSetting) — a partial body 403s with "Unable to resolve application deployment setting". A masked secret read back from GET (e.g. AHSW's perimeterServerFixedKey: "***") must be replaced with the real value or it is overwritten. The role does not PATCH groups (create-if-absent + nightly rebuild); used only for out-of-band live fixes.
api/groups/v1/{application}/{groupId}/machines POST (201) / DELETE (204) Group membership — see Group machine, environment, and package membership. GET is unsupported (405).
api/machines/v3 GET/POST (200/201) Machine records. Create body is a single JSON:API primary object {type: "Machine", attributes: {name, machineType, requestedMaintenanceMode: "In service", associatedApplications: [...display names...]}}not wrapped in data, and omitting requestedMaintenanceMode 500s with "Maintenance mode expected and not defined". associatedApplications uses display names with spaces (Hyperspace Web, Interconnect, Kuiper, Hyperspace); an unknown name 400s, an empty list is accepted. Create does not enforce DNS resolution.
api/v1/machines/{id} PATCH/PUT/DELETE Update (body type Server, ServerSchema) or delete one machine by id. Used to clean up test records.
api/v1/maintenance/availableStatuses GET (200) Maintenance enum: 0 NotUsed, 1 InService, 2 TransitioningOutOfService, 3 OutOfService.
api/v1/settings/* GET/PUT/POST Granular internal toggles with no public equivalent — CDN download source, key rotation, EPS font embedding, credentials auto-logon, Hyperdrive publish-delay max, system-assessment child-machine count, etc.
api/v1/deployment/* GET/PUT Deployment tuning — psOpenTimeoutOverride, skipScannedPrerequisites, sqlConnectionRunnerDefaultMaxAttempts, winRmHttpsOverrides.
api/v1/distributed/* GET/PUT Agent tuning — action-retrieval settings, minutes-between-actions, max simultaneous downloads.
api/v1/executepowershellscript/machine/{machineId} POST Runs a registered PowerShell extension command on a machine (body requires ModuleName + command, same shape as Deployment Extensions — not arbitrary script text). Powerful; gate behind explicit opt-in.
api/v1/Groups/HyperspaceWeb/EnablePerimeterAuthenticationUI?enable=true POST (204) Exposes the "Set as perimeter authentication server" checkbox on Hyperspace Web groups instance-wide (Epic's Enable-PerimeterHSWebUI.ps1). Requires an empty JSON body ({}; a body-less POST 411s, like Satellite publish) and returns 204 No Content. Only unhides the option — the per-group toggle is deploymentSetting.isPerimeterServer on the group create body (requires allowOAuth2Authorization: false + a non-blank perimeterServerFixedKey; see API Configuration). No GET to read the current UI-enablement state, so idempotency is limited to "POST only when the operator opts in"; Kuiper treats a repeat enable as a no-op. Driven by kuiper_enable_perimeter_hsw_ui.

Group machine, environment, and package membership

Assigning machines, environments, and packages to a group are REST actions, so the role uses the REST routes for every application, Interconnect included. roles/kuiper/tasks/fleet/group_machines.yml, roles/kuiper/tasks/fleet/group_environments.yml, and roles/kuiper/tasks/fleet/group_packages.yml drive them.

Action Request Result
Add machines POST api/groups/v1/{application}/{groupId}/machines?isDowntimeTolerant=true, body a bare JSON array of machine ids ([3, 4]) 201, body {"data":[{"id":3,"type":"Machine"}]}
Remove machines DELETE …/machines, same bare-array body 204
Read members GET api/v1/servers?groupId={groupId} member Server records (the …/machines route rejects GET with 405)
Add environments POST api/groups/v1/{application}/{groupId}/environments, body a bare JSON array of environment ids ([3]) 201, body {"data":[{"id":3,"type":"Environment"}]}
Remove environments DELETE …/environments, same bare-array body 204
Read attachments GET api/groups/v1/{application}/{groupId}data[0].relationships.environments.ids the key is absent when no environments are attached (the …/environments route rejects GET with 405; GET api/v1/environments?groupId={id} also works)
List packages GET api/packages/v1/{application} {data:[{id, attributes:{applicationName, version, longVersion, ras, dateAdded, packageFilePaths}}]} — the packages Kuiper has scanned from the Base Package Source. The bare api/packages/v1 404s, but the collection route with its trailing slash works: api/packages/v1/?applicationName=Hyperspace Web (spaces are accepted here, unlike the group routes). The v2 routes work tooapi/packages/v2/{application}, /v2/{packageId}, /v2/?applicationName=… — and rename the ras attribute to raInfo, so remap that field if you version-switch.
Deploy packages POST api/groups/v1/{application}/{groupId}/packages?isDowntimeTolerant={bool}&ignoreWarnings={bool}[&isStaging={bool}], body a bare JSON array of package ids ([2]) 201, body {"data":[{"id":2,"type":"Package"}]}. isStaging stages instead of deploying — see Staging a package instead of deploying it
Uninstall packages DELETE …/packages, same bare-array body 204
Read package status GET api/groups/v2/{application}/{groupId}/packages, or …/packages/{packageId} for one 200, one GroupPackageStatus per associated package, carrying attributes.isActive / attributes.isStaging and the deploy action's id, description and status. v2 only — the same path on v1 rejects GET with 405, which is why this route was long recorded here as write-only
Read deployed package ids GET api/groups/v1/{application}/{groupId}data[0].relationships.packages.ids same id space as the catalog id; key absent when none. Lists associations but cannot distinguish staged from active — use the v2 status route for that

Gotchas, all confirmed live:

  • The body is a top-level System.Int32[] — send ids | to_json ([3]). Any wrapping object ({machineIds: […]}, JSON:API {data:[…]}) 400s with "requires a JSON array"; an Ansible YAML-list body doesn't serialize and posts as empty.
  • Adding a member that is already present returns 403 with {"errors":[{"message":"No Machine has been added. None were provided."}, …]} (environments and packages word it identically). So membership is idempotent by reading first and posting only the absent ids — never by catching the 403.
  • Group id is resolved by name from GET api/groups/v1/{application}; machine ids from GET api/v1/servers. The machines must already be registered.
  • The {application} segment on the group routes is constrained :alpha? — letters only. api/groups/v1/hyperspaceweb works (case-insensitive) but api/groups/v1/Hyperspace%20Web 404s. The machines, packages, and validation routes do accept the spaced display name, so don't share one name-formatting helper across them.
  • A Hyperspace Web environment must carry an EMP record (its own empId or the configured default EMP) to attach; Interconnect environments need none.
  • isDowntimeTolerant (on the machines, environments, servers, and packages sub-routes) is the "install prerequisites" control: true lets Kuiper install prerequisites and reboot member machines as needed to complete the action; false does neither. ignoreWarnings=true (packages) proceeds past voided release-authorization (RA) warnings instead of stalling for acknowledgement. The role adds machines and deploys packages with isDowntimeTolerant=true.

Staging a package instead of deploying it (isStaging)

The package sub-route takes a fourth, undocumented query parameter, isStaging, which distributes a package's files to a group's machines without installing or activating it — the REST equivalent of the Stage button on the Kuiper UI's packages table. The full action signature, read off the deployed GroupsApiController, is:

AddPackages(string application, int id, [FromBody] int[] pkgIds,
            bool isDowntimeTolerant = false,   // install prerequisites, reboot as needed
            bool ignoreWarnings = false,       // proceed past voided-RA warnings
            bool isStaging = false)            // stage instead of deploy/publish

So a stage is the ordinary deploy call plus one flag, on the same bare-id-array body:

POST api/groups/v1/{application}/{groupId}/packages?isStaging=true&isDowntimeTolerant=false
[12]
→ 201 {"data":[{"id":12,"type":"Package"}]}

Both v1 and v2 accept the flag (confirmed live), and the action requires the per-application DeployApplicationSecurityPoint ("Deploy a Group") security point, exactly like a deploy.

Staging is a stage in a schedule, not a boolean. Client (Satellite-managed, "distributed") applications publish through distributed action sets, and each set advances through a two-value stage enum, Epic.Release.Management.Distributed.Staging.DistributedActionStage: Staging = 10, Execution = 20. In the database, DistributedActionSets holds one row per publish, joined to groups through Groups_DistributedActionSets, with DistributedStageActiveTimes(DistributedActionSet_Id, Stage, ActiveTime) recording when each stage becomes active. A package is staged while the set's highest active stage is Staging, and active once it reaches Execution — which is what lets an operator stage now and activate later.

Only some applications support it. Each application's IPublishDefinition declares SupportsStagedPackages. In Kuiper 118.0 the applications that support staged packages are Auto Local Printing, BCA PC, Enterprise Monitoring Collector, EpicCare Remote Client, Epic Monitor, Hyperdrive Registration Only, Kuiper, Satellite Only, WarpDrive, Welcome, Willow Ambulatory and WPF App Launcher. Two caveats sit on that list:

  • Hyperdrive does not stage. Its publish definition sets PublishesWithChannels: true instead, so its staged-rollout mechanism is update channels and rings — api/channels/v1/{id}/authorization/{packageId}, …/{id}/activation/{packageId}, and PATCH …/{id}/scheduledRings (a ChannelRingSchemaV1 with ActivationTime, Percentage, Paused and MachineTagIds).
  • BCA PC, Epic Monitor, Welcome and Willow Ambulatory report PublishSupport: RequiresConversion — their groups must be converted to distributed before a publish or stage will work, and the only route for that is MVC POST Groups/ConvertToDistributed {groupId}.

The server applications this role deploys — Hyperspace Web and Interconnect — have no publish support at all (PublishSupport: None); they use push orchestration. That is where the one dangerous behaviour lives:

isStaging is silently ignored on an application that does not support staging

Confirmed live against a throwaway Hyperspace Web group with no member machines: POST api/groups/v1/HyperspaceWeb/{id}/packages?isStaging=true with a package that genuinely belongs to that application returned 201, and the v2 status read-back came back isStaging: false with a queued action described "Deploying Hyperspace Web package 118.0.0 to …". The flag was accepted, ignored, and a normal deployment started — with no error and no warning. Had the call also passed isDowntimeTolerant=true, Kuiper would have installed prerequisites and rebooted the group's machines.

So any caller must check SupportsStagedPackages for the target application itself before posting, and confirm the outcome by reading api/groups/v2/{application}/{groupId}/packages and asserting isStaging: true. Never rely on the server to reject a staging request.

The MVC path fails closed by comparison, which makes it a usable pre-flight: against an application with no publish support, both Publish/CanPublish and Publish/StagePackage return {"Success": false, "Message": "Specified method is not supported."}. It keys off the package's application, though, so it also rejects a valid group handed a package from another application.

isStaging is not used by the rolefleet/group_packages.yml deploys, and the IRE fleet is Hyperspace Web, Interconnect, Kuiper and Satellite Only groups. It is documented here because it is the supported REST route for staging and because discovering it corrected the read-back rows above. Two things about it remain unverified, for want of a package belonging to a staging-capable application in the dev environment: that isStaging=true on a supporting application reads back isStaging: true with a "Staging …" action, and how activation of an already-staged package is expressed. The database procs (UpdateStagePublishScheduleForSet, BuildStageSchedule, CheckStageActiveTimesAreNotDecreasing) suggest a later publish updates the existing set's Execution activation time rather than creating a second set — meaning "activate" is a plain publish against the same group and package. Confirm that before building anything on it, because the usual "skip when the package id is already associated" idempotency guard would wrongly skip such a call.

Security groups (api/security-groups/v1)

New in Kuiper August 2026 (119.x); found by reflecting over Epic.Release.Management.Web.dll for SecurityGroupsApiController. It replaces the entire Security/SettingsSecurity/FindAccountsSecurity/Save MVC flow the role used previously, for both kuiper_security_groups and the Super Administrator bootstrap.

Action Request Result
List GET api/security-groups/v1 [{Id, Name, Description}]
Read one GET api/security-groups/v1/{id} full model
Create POST api/security-groups/v1 201, full model
Replace PUT api/security-groups/v1/{id} 200
Delete DELETE api/security-groups/v1/{id} 204
Add members POST api/security-groups/v1/{id}/members 200, the member list
Replace members PUT api/security-groups/v1/{id}/members
Remove members POST api/security-groups/v1/{id}/members/remove
Point catalog GET api/security-groups/v1/template InfrastructureSecurityPoints, ApplicationSecurityPoints, DeviceSecurityPoints

The body is the flat model, not JSON:API-wrapped:

{
  "Name": "ODB Admins",
  "Description": "Read/edit Environments and read/edit/run Environment Upgrades",
  "JeaSupport": false,
  "ApplicationGroupingType": "EnvironmentType",
  "ApplicationEnvironmentTypes": ["Production", "NonProduction", "Training"],
  "InfrastructureSecurityPoints": [{"Name": "ReadEnvironmentInfrastructureSecurityPoint"}],
  "ApplicationSecurityPoints": [
    {"Application": "Upgrade Console", "SecurityPoints": [{"Name": "CreateRunEnvUpgrades"}]}
  ],
  "DeviceSecurityPoints": {"EnvironmentTypes": [], "SecurityPoints": []}
}

Three behaviours worth knowing before automating against it, all verified live:

  • Members is ignored on create. However you populate it, the create response comes back Members: []. Members land only through the /members sub-route, so a create is always two calls.
  • Bare account names resolve server-side. Posting [{"Name": "ODB Admins"}] stores AADDS\ODB Admins, so no account-search step and no domain-prefix configuration are needed. Since the stored form is qualified and the desired form usually is not, an add-if-absent comparison has to match on the whole string or on the part after the last backslash.
  • Permission dependencies auto-expand, exactly as the UI auto-checks them: a group saved with only CreateRunEnvUpgrades reads back with ReadApplicationSecurityPoint too. Idempotency must be desired ⊆ actual, not set equality.

Permissions are identified by security-point Name — the same strings the template catalog lists — so the per-instance runtime integer ids that the MVC model required are no longer involved anywhere.

MVC-driven actions

Some actions the web UI performs have no REST API and are reachable only through the Forms-authenticated MVC controllers. The role automates two — certificate import and deployment to machines, and System Pulse client registration — because the REST API cannot do them. These routes and model shapes are internal and may change between Kuiper builds.

The list shrinks as Kuiper ships routes. Interconnect group creation, Super Administrator membership and security-group creation left it in Kuiper August 2026 (119.x) and are now plain REST: group creation via POST api/groups/v1/Interconnect (above) and the two security-group features via the new api/security-groups/v1 controller.

The shared flow (roles/kuiper/files/kuiper_mvc_request.ps1):

  1. Log inGET /Kuiper/Account/Login for the anti-forgery cookie and hidden __RequestVerificationToken, then POST the credentials. The login form's CanvasFingerprint field is an MFA trusted-device fingerprint, not a login gate, so an empty value authenticates non-MFA accounts. Login sets a .ASPXAUTH ticket, and is retried since it is occasionally refused.
  2. Get a CSRF tokenGET an authenticated page and read its __RequestVerificationToken.
  3. Request — POST/DELETE the action, sending the token as the X-Csrf-Token header (JSON posts don't carry the form field).

Notes:

  • HttpClient / Invoke-WebRequest fail the TLS send against this endpoint; HttpWebRequest with Expect100Continue = $false and KeepAlive = $false works.
  • An MVC save can reject an invalid model with HTTP 200, Success:false, and no message, so callers key success on a specific field (e.g. Data.GroupID for a created group) rather than the status code.
  • The MVC login account must be non-MFA, like the REST API account.

Endpoints per feature

  • Interconnect group creationGroups/SaveWithValidate with the group model as JSON. DistributedMetricDefinitionId is a required field (reference data, one row per application type in I_DistributedMetricDefinitions); SystemPulseUrlId may be null; an empty Instances map yields a valid empty group; a tag that doesn't already exist is rejected. Instances is a map of instance name → IsChroniclesConfigured (bool), not enabled — every instance in it persists as one SecondaryApplicationGroups entry named <group>-<instance> (Kuiper stores the instance-name suffix) with a negative placeholder Id (the server assigns a real id on save), so a non-Chronicles instance is listed with false; the parent Instances map alone does not stick.
  • Certificate importCertificates/Upload ({base64RawData, passphrase}), listing with Certificates/GetNCertificates, deleting with Certificates/Delete. The Kuiper server / app-pool TLS certificate is still managed by the certificate_authority role.
  • Certificate deployment to machinesCertificates/AddCertificateToServer ({certificateId, serverIds, markPrivateKeyExportable, siteName, port, hostName, requireSni}) pushes a stored certificate to machines and, when siteName/port are non-null, binds it to that IIS site (null site/port deploys without a binding). Server ids come from Certificates/GetServersAndApplications; the reverse operation is Certificates/DeleteCertificateFromServer ({certificateId, serverIds, deleteCertificate}), and Certificates/ScanServers ({ServerId}) inventories certificates already on a machine. Kuiper performs the deployment synchronously over a PSSession (WinRM, HTTP by default — see api/v1/deployment/winRmHttpsOverrides) from the Kuiper server to the machine, authenticated with a stored Windows credential associated with the machine's application — that account must be a local Administrator on the target, and an IIS binding requires IIS to be installed there. Per-server failures are reported inside a Success:true envelope: success for a server means it appears in Data.SuccessServers; reasons land in Data.FailureMessages. The certificate list's AssociatedServers ({Id, Name, SiteName, Port, HostName, RequireSni}) is the current deployment/binding state.
  • System Pulse client registrationGET Settings/KuiperAuth returns {Data: {ClientId, Secret}}, the credentials System Pulse must know to accept API calls from Kuiper (both auto-generated at install; POST Settings/RegenerateSystemPulseApiSecret rotates the secret). Related: GET Settings/KuiperId, GET Settings/SystemPulseApiSecret, and GET Settings/SystemPulseApiAuthUrl/{id} (builds the SP settings-page link the UI opens). The System Pulse half of the handshake is not Kuiper at all: POST <SP>/Admin/ApiAuth.aspx/GetSettingList (body {}) and POST <SP>/Admin/ApiAuth.aspx/SaveSettingList (body {"settingArray": [{Id, FriendlyName, ClientId, Secret, Salt}]}) — ASP.NET page methods behind System Pulse's Forms login (Login/Login.aspx, AD membership provider, sAMAccountName usernames; carry the .ASPXAUTH cookie). System Pulse never returns stored secrets; re-saving an existing entry with an empty Secret keeps the stored one, and a new entry's secret must be ≥10 characters.
  • (No longer automated, kept for reference) Group PowerShell impersonation credential — the group model's PowerShellImpersonationCredentialId / DistributedImpersonationCredentialId (MVC-only, via Groups/GetOneGroups/SaveWithValidate, set-at-create-time only) can pin a per-group remote-PowerShell deploy identity. The role's former feature for it was removed 2026-07-24: a workgroup machine is reached as the logged-in API identity instead (local-account login + enableNtlm + TrustedHosts), so no per-group identity is needed. The implementation survives in git history.

Deliberately not automated

  • api/epic/* — Interconnect clinical passthrough, unrelated to deployment config.
  • api/distributed/v1/* — Satellite/agent-facing (auth, downloads, ping, versioning, printing); called by agents, not by us.

Reproducing the scan

The scan is host-local (the control node cannot reach the Kuiper host's 443 in the IRE environment). Run it over WinRM against the target:

# On the Kuiper host — list the public API controllers
Get-ChildItem 'C:\Epic\ECSM Tools\Web\bin' -Filter *.dll | ForEach-Object {
  $t = [Text.Encoding]::Unicode.GetString([IO.File]::ReadAllBytes($_.FullName))
  [regex]::Matches($t, '[A-Za-z][A-Za-z0-9]{2,60}ApiController') | ForEach-Object { $_.Value }
} | Sort-Object -Unique