Skip to content

API Reference ​

The reference for calling the Manager from code: the GraphQL queries and mutations with working examples, then the REST routes with their request and response shapes. If a field is not here, check the schema in the playground before using it.

The Manager exposes two API surfaces with distinct jobs:

  • GraphQL (POST /query) is the management API: projects, tenants, applications, environments, users, roles, settings, deployment specs, and triggers. It is the same API the Manager UI and the pfy CLI use; authenticate with a PAT.
  • REST covers machine-to-machine and delivery concerns: backend registration (/api/machine/*), proxy validation (/api/validate-*), CLI login (/api/cli/login/* plus the browser consent page at /cli/authorize), frontend configuration, language packs, the pilet feed, the comms inbox, the scale-from-zero activator, compliance export, and the ESB.

Base URLs ​

  • GraphQL endpoint: POST /query
  • GraphQL playground (dev only): GET /gql
  • REST: /api/*, /cli/authorize, /pilets/*, /pilet-feed/*, /pilet-bundle/*, /pilet-keys/*, /language-packs/*, and /esb/*
  • Health server (separate port, default 8081): GET /healthz, GET /readyz
  • API port: GET/HEAD /health, the same readiness verdict /readyz gives

Paths are absolute and already carry their own prefix; the base URL is the host alone. See the REST route map for the complete set and the authentication scheme that applies to each.

Authentication ​

The Manager supports three authentication methods:

1. Bearer Token (Personal Access Tokens) ​

Personal Access Tokens (PATs) provide secure API access for users. PATs authenticate requests to the GraphQL endpoint (/query) and the pilet REST endpoints (/pilets/*). Send them through the proxy host: both routes check the proxy shared secret before reading the token, and a request sent straight to the Manager port is answered 401 with a JSON error explaining why (see Error Handling).

Header Format:

http
Authorization: Bearer <your-token>

Creating a PAT:

graphql
mutation {
  createPersonalAccessToken(
    name: "My API Token"
    expiresAt: "2026-12-31T23:59:59Z"
  ) {
    token
    personalAccessToken {
      id
      name
      tokenPrefix
      expiresAt
      createdAt
    }
  }
}

WARNING

The token is only shown once during creation and cannot be retrieved later. Store it securely.

2. Machine User Authentication ​

Machine users enable backend services to authenticate with the Manager. Machine user credentials authenticate the /api/machine/* and /esb/* endpoints (not the GraphQL API).

Bearer Token:

http
Authorization: Bearer <machine-user-token>

Bearer only on the Manager's own routes

/api/machine/* and /esb/* accept Bearer tokens only; a Basic header is rejected with 401 {"error":"Invalid authorization format. Expected: Bearer <token>"}. Basic-auth machine users (username + password) are validated by the proxy on application routes, via POST /api/validate-proxy-auth.

Creating a Machine User:

graphql
mutation {
  createMachineUserWithCredentials(
    environmentId: "environment-uuid"
    input: {
      name: "Backend Service"
      username: "Bearer"
      hashedKey: "__GENERATE_TOKEN__"
      enabled: true
    }
  ) {
    generatedToken
    machineUser {
      id
      username
    }
  }
}

A machine user belongs to an environment, not a tenant. With hashedKey: "__GENERATE_TOKEN__" the Manager generates the token and replaces username with Bearer:<token prefix>, so the value you pass is ignored. For a Basic-auth machine user pass the real username and the plaintext password as hashedKey; the Manager hashes it and echoes it back once as plaintextPassword.

3. User Session (OAuth/OIDC) ​

Standard web session authentication. OAuth/OIDC login is handled by the Proxy, which validates the session with the Manager and forwards the user's identity on each request.


GraphQL API ​

Schema Overview ​

The GraphQL API provides type-safe access to all Manager resources with filtering, ordering, and pagination support.

GraphQL Playground: Access at /gql in development mode to explore the schema interactively.

Common Types ​

Filter ​

graphql
input Filter {
  field: String!
  value: String!
}

Order ​

graphql
input Order {
  field: String!
  direction: OrderDirection!
}

enum OrderDirection {
  ASC
  DESC
}

Pagination ​

graphql
input Pagination {
  limit: Int!
  offset: Int!
}

Allowed Limits

limit must be one of 1, 10, 20, 25, 50, or 100. Other values are rejected.

Endpoint Types ​

Authentication Types:

graphql
enum EndpointAuthType {
  Basic # HTTP Basic Authentication
  Bearer # Bearer Token Authentication
  None # No authentication required
}

Message Types:

graphql
enum EndpointInputMessageType {
  SOAP # SOAP XML message
  XML # Plain XML
  JSON # JSON payload
}

enum EndpointOutputMessageType {
  SOAP # SOAP XML message
  XML # Plain XML
  JSON # JSON payload
}

Log Levels:

graphql
enum EndpointLogLevel {
  NONE # No logging
  BASIC # Log metadata only (method, path, status, timing); no headers or bodies
  FULL # Log full request/response bodies
}

Queries ​

Projects ​

List Projects ​

graphql
query {
  projects(
    filters: []
    order: { field: "createdAt", direction: DESC }
    pagination: { limit: 10, offset: 0 }
  ) {
    id
    name
    slug
    createdAt
  }
}

Get Project by Slug ​

graphql
query {
  project(slug: "my-project") {
    id
    name
    slug
    tenants {
      id
      name
    }
    applications {
      id
      name
    }
  }
}

Tenants ​

List Tenants ​

graphql
query {
  tenants(
    projectSlug: "my-project"
    filters: []
    order: { field: "name", direction: ASC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    name
    slug
    deploymentMode
  }
}

Get Tenant ​

graphql
query {
  tenant(projectId: "project-uuid", slug: "tenant-slug") {
    id
    name
    slug
    deploymentMode
    project {
      id
      name
    }
  }
}

A tenant has no applications field: applications belong to the project and are enabled per tenant. List them through project { applications { ... } } or applications(projectSlug: ...).

Applications ​

List Applications ​

graphql
query {
  applications(
    projectSlug: "my-project"
    filters: []
    order: { field: "name", direction: ASC }
    pagination: { limit: 10, offset: 0 }
  ) {
    id
    name
    slug
    description
    endpoints {
      id
      name
      url
    }
  }
}

Get Application ​

graphql
query {
  application(projectId: "project-uuid", slug: "app-slug") {
    id
    name
    slug
    description
    triggers {
      id
      name
      cronExpression
      enabled
    }
  }
}

Settings ​

Get Settings ​

graphql
query {
  settings(
    project: "project-uuid"
    tenant: "tenant-uuid"
    application: "app-uuid"
  ) {
    id
    backendSetting {
      id
      moduleConfig {
        EnabledModules
        KeyValuePairs {
          Key
          Value
        }
      }
    }
    frontendSetting {
      id
      moduleConfig {
        EnabledThemes
        DefaultTheme
        EnabledLocales
        DefaultLocale
      }
    }
  }
}

Configuration Inheritance

Settings cascade through four layers: project, application (module fields only), tenant, and the tenant × application join, the most specific. Exactly one selector (project, tenant, application or tenant2application) names the layer, and the query returns the settings row attached there. The effective merged configuration for one tenant is served by the REST endpoint GET /api/frontend-config/:application_id/:tenant_id (proxy-only). See Configuration Layers.

Endpoints ​

List Endpoints ​

graphql
query {
  endpoints(
    application: "app-uuid"
    filters: []
    order: { field: "name", direction: ASC }
    pagination: { limit: 50, offset: 0 }
  ) {
    id
    name
    description
    enabled
    url
    consumeURL
    authType
  }
}

Triggers ​

List Triggers ​

graphql
query {
  triggers(
    application: "app-uuid"
    filters: []
    order: { field: "name", direction: ASC }
    pagination: { limit: 50, offset: 0 }
  ) {
    id
    name
    cronExpression
    enabled
    createdAt
    updatedAt
  }
}

Machine Users ​

List Machine Users ​

graphql
query {
  machineUsers(
    environment: "environment-uuid"
    filters: []
    order: { field: "name", direction: ASC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    name
    username
    enabled
    tokenPrefix
  }
}

Language Packs ​

List Language Packs ​

graphql
query {
  languagePacks(
    project: "project-uuid"
    filters: []
    order: { field: "name", direction: ASC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    name
    content {
      Key
      Value
    }
  }
}

Personal Access Tokens ​

List My Personal Access Tokens ​

graphql
query {
  myPersonalAccessTokens(
    filters: []
    order: { field: "createdAt", direction: DESC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    name
    tokenPrefix
    expiresAt
    createdAt
    lastUsedAt
  }
}

List Personal Access Tokens (Cursor-based) ​

graphql
query {
  personalAccessTokens(first: 20, after: "cursor-string") {
    edges {
      node {
        id
        name
        tokenPrefix
        expiresAt
        createdAt
        lastUsedAt
      }
      cursor
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
    totalCount
  }
}

Token Security

Only the token prefix is visible after creation. The full token is only shown once during creation.

Users ​

Get Current User ​

graphql
query {
  me {
    id
    externalName
    externalUsername
    externalEmail
    role
  }
}

List Users ​

graphql
query {
  users(
    filters: []
    order: { field: "externalName", direction: ASC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    externalName
    externalUsername
    externalEmail
    role
  }
}

Search Provider Users ​

graphql
query {
  userProviderSearch(name: "john") {
    id
    username
    email
    displayName
    disabled
  }
}

User Relationships ​

List User-Application Relations ​

graphql
query {
  user2applications(
    application: "app-uuid"
    filters: []
    order: { field: "createdAt", direction: DESC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    role
    user {
      id
      externalUsername
      externalEmail
    }
    application {
      id
      name
    }
  }
}

Get User-Application Relation ​

graphql
query {
  user2application(application: "app-uuid", id: "relation-uuid") {
    id
    role
    user {
      externalUsername
    }
  }
}

List User-Project Relations ​

graphql
query {
  user2projects(
    project: "project-uuid"
    filters: []
    order: { field: "createdAt", direction: DESC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    role
    user {
      id
      externalUsername
      externalEmail
    }
    project {
      id
      name
    }
  }
}

Get User-Project Relation ​

graphql
query {
  user2project(project: "project-uuid", id: "relation-uuid") {
    id
    role
    user {
      externalUsername
    }
  }
}

List User-Tenant Relations ​

graphql
query {
  user2tenants(
    tenant: "tenant-uuid"
    filters: []
    order: { field: "createdAt", direction: DESC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    roleName
    user {
      id
      externalUsername
      externalEmail
    }
    tenant {
      id
      name
    }
  }
}

Get User-Tenant Relation ​

graphql
query {
  user2tenant(tenant: "tenant-uuid", id: "relation-uuid") {
    id
    roleName
    user {
      externalUsername
    }
  }
}

Machine User to Endpoint Relations ​

List Machine User-Endpoint Relations ​

graphql
query {
  machineUser2endpoints(
    endpoint: "endpoint-uuid"
    filters: []
    order: { field: "createdAt", direction: DESC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    enabled
    machineUser {
      id
      username
    }
    endpoint {
      id
      name
    }
  }
}

Get Machine User-Endpoint Relation ​

graphql
query {
  machineUser2endpoint(endpoint: "endpoint-uuid", id: "relation-uuid") {
    id
    enabled
  }
}

Audit Logs ​

List Audit Logs ​

graphql
query {
  auditLogs(
    projectID: "project-uuid"
    filters: [{ field: "action", value: "create" }]
    order: { field: "createdAt", direction: DESC }
    pagination: { limit: 50, offset: 0 }
  ) {
    id
    entityType
    entityID
    action
    description
    metadata
    changes
    ipAddress
    userAgent
    createdAt
    user {
      externalUsername
    }
  }
}

Pilet Packages ​

List Pilet Packages ​

graphql
query {
  piletPackages(projectId: "project-uuid", enabled: true) {
    packages {
      id
      name
      version
      description
      size
      hash
      enabled
      createdAt
      updatedAt
    }
    count
  }
}

Get Pilet Package ​

graphql
query {
  piletPackage(
    projectId: "project-uuid"
    name: "@my-org/my-pilet"
    version: "1.0.0"
  ) {
    id
    name
    version
    description
    size
    hash
    enabled
    createdAt
    updatedAt
  }
}

Mutations ​

Project Management ​

Create Project ​

graphql
mutation {
  createProject(input: { name: "My Project", slug: "my-project" }) {
    id
    name
    slug
  }
}

Update Project ​

graphql
mutation {
  updateProject(
    id: "project-uuid"
    input: { name: "Updated Project Name", slug: "updated-project" }
  ) {
    id
    name
    slug
  }
}

Delete Project ​

graphql
mutation {
  deleteProject(id: "project-uuid")
}

Tenant Management ​

Create Tenant ​

graphql
mutation {
  createTenant(
    projectId: "project-uuid"
    input: { name: "Customer A", slug: "customer-a" }
  ) {
    id
    name
    slug
    deploymentMode
  }
}

Update Tenant ​

graphql
mutation {
  updateTenant(id: "tenant-uuid", input: { name: "Updated Customer Name" }) {
    id
    name
  }
}

Delete Tenant ​

graphql
mutation {
  deleteTenant(id: "tenant-uuid")
}

Application Management ​

Create Application ​

graphql
mutation {
  createApplication(
    projectId: "project-uuid"
    input: {
      name: "Web App"
      slug: "web-app"
      description: "Main web application"
    }
  ) {
    id
    name
    slug
  }
}

Update Application ​

graphql
mutation {
  updateApplication(
    id: "app-uuid"
    input: { description: "Updated description" }
  ) {
    id
    description
  }
}

Delete Application ​

graphql
mutation {
  deleteApplication(id: "app-uuid")
}

Promote Application ​

promoteApplication carries a @deprecated directive in the schema and always returns an error: applications belong to the project, not to a tenant, so there is nothing to promote between tenants. Copy per-(tenant, application) configuration instead:

graphql
mutation {
  copyTenant2ApplicationConfig(
    input: {
      sourceTenant2ApplicationId: "staging-join-uuid"
      targetTenant2ApplicationId: "prod-join-uuid"
    }
  ) {
    id
  }
}

Both join rows must be manageable by the caller. Look them up with tenant2Applications(applicationId: ..., tenantId: ...).

Copy Application ​

copyApplication carries the same directive for the same reason and always returns an error. Use copyTenant2ApplicationConfig as above.

Settings Management ​

Configuration Inheritance

A Settings container links a BackendSettings and a FrontendSettings row to one layer of the cascade: project, application, tenant, or tenant × application. There is no createSettings mutation; the Manager creates the rows when a project, tenant or application is created, and ensureTenant2ApplicationSettings(tenant2ApplicationId:) creates the leaf on demand. Configuration values are managed with the updateBackendModuleConfig and updateFrontendModuleConfig mutations below.

Delete Backend Settings ​

graphql
mutation {
  deleteBackendSettings(id: "backend-settings-uuid")
}

Delete Frontend Settings ​

graphql
mutation {
  deleteFrontendSettings(id: "frontend-settings-uuid")
}

Update Backend Module Configuration ​

graphql
mutation {
  updateBackendModuleConfig(
    id: "backend-settings-uuid"
    input: {
      EnabledModules: ["module1", "module2"]
      KeyValuePairs: [
        { Key: "feature_flag_1", Value: "true" }
        { Key: "api_timeout", Value: "30" }
      ]
    }
  ) {
    id
    moduleConfig {
      EnabledModules
      KeyValuePairs {
        Key
        Value
      }
    }
  }
}

Update Frontend Module Configuration ​

graphql
mutation {
  updateFrontendModuleConfig(
    id: "frontend-settings-uuid"
    input: {
      EnabledThemes: ["dark", "light"]
      DefaultTheme: "light"
      EnabledLocales: ["en", "hu"]
      DefaultLocale: "en"
      KeyValuePairs: [{ Key: "show_footer", Value: "true" }]
    }
  ) {
    id
    moduleConfig {
      DefaultTheme
      DefaultLocale
    }
  }
}

Language Pack Management ​

Create Language Pack ​

graphql
mutation {
  createLanguagePack(projectId: "project-uuid", input: { name: "en-US" }) {
    id
    name
  }
}

Update Language Pack Content ​

graphql
mutation {
  updateLanguagePackContent(
    id: "language-pack-uuid"
    input: {
      content: [
        { Key: "welcome", Value: "Welcome" }
        { Key: "login", Value: "Login" }
        { Key: "logout", Value: "Logout" }
      ]
    }
  ) {
    id
    content {
      Key
      Value
    }
  }
}

Endpoint Management ​

Create Endpoint ​

graphql
mutation {
  createEndpoint(
    applicationId: "app-uuid"
    input: {
      name: "Webhook Handler"
      description: "Processes incoming webhooks"
      enabled: true
      consumeURL: "/api/webhook"
      url: "https://backend.example.com/webhook"
      authType: Bearer
      inputMessageType: JSON
      outputMessageType: JSON
      logLevel: BASIC
    }
  ) {
    id
    name
    consumeURL
    url
    authType
    inputMessageType
    outputMessageType
    logLevel
  }
}

Field Descriptions:

  • consumeURL - The inbound path that selects this endpoint (required): a rooted path such as /api/webhook, unique within the application; incoming /esb requests are matched against it exactly
  • url - The absolute http:// or https:// backend URL the ESB forwards to; a request to an endpoint without one answers 502
  • authType - Authentication method: Basic, Bearer, or None (default: None)
  • inputMessageType - Expected input format: JSON, XML, or SOAP (optional)
  • outputMessageType - Output format after transformation: JSON, XML, or SOAP (optional)
  • logLevel - ESB logging verbosity: NONE, BASIC, or FULL (default: BASIC)

Update Endpoint ​

graphql
mutation {
  updateEndpoint(
    id: "endpoint-uuid"
    input: {
      enabled: false
      url: "https://new-backend.example.com/webhook"
      logLevel: FULL
    }
  ) {
    id
    enabled
    url
    logLevel
  }
}

Delete Endpoint ​

graphql
mutation {
  deleteEndpoint(id: "endpoint-uuid")
}

Trigger Management ​

Create Trigger ​

graphql
mutation {
  createTrigger(
    applicationId: "app-uuid"
    input: {
      name: "Daily Report"
      description: "Generates daily report"
      cronExpression: "0 0 * * *"
      runKey: "daily-report"
      enabled: true
    }
  ) {
    id
    name
    cronExpression
    runKey
  }
}

Update Trigger ​

graphql
mutation {
  updateTrigger(
    id: "trigger-uuid"
    input: { enabled: false, cronExpression: "0 2 * * *" }
  ) {
    id
    enabled
    cronExpression
  }
}

Machine User Management ​

Create Machine User with Token ​

graphql
mutation {
  createMachineUserWithCredentials(
    environmentId: "environment-uuid"
    input: {
      name: "API Service"
      username: "Bearer"
      hashedKey: "__GENERATE_TOKEN__"
      enabled: true
    }
  ) {
    generatedToken
    machineUser {
      id
      username
      tokenPrefix
    }
  }
}

Token Generation

Use hashedKey: "__GENERATE_TOKEN__" to auto-generate a Bearer token. The plaintext token is only returned once, and the stored username becomes Bearer:<token prefix> regardless of what you passed.

Rotate Machine User Token ​

graphql
mutation {
  rotateMachineUserToken(id: "machine-user-uuid") {
    generatedToken
    machineUser {
      id
      username
      tokenPrefix
      lastRotatedAt
    }
  }
}

The only way to change a machine user's stored credential. It mints a fresh Bearer credential inside the request transaction, writes a rotate audit row with key_rotated: true, and returns the plaintext in generatedToken exactly once. The previous credential stops verifying immediately. Requires the project role maintainer or owner, or the system role administrator. A Bearer machine user's username is re-derived as Bearer:<new prefix>; a Basic machine user keeps its username and generatedToken becomes its password. See Machine users.

Update Machine User ​

graphql
mutation {
  updateMachineUser(id: "machine-user-uuid", input: { enabled: false }) {
    id
    enabled
  }
}

Takes name and enabled, and writes an update audit row. It refuses hashedKey, tokenPrefix and clearTokenPrefix with an error naming rotateMachineUserToken: a caller-supplied value is ambiguous between a hash and a password, and either reading leaves a credential that cannot authenticate.

Delete Machine User ​

graphql
mutation {
  deleteMachineUser(id: "machine-user-uuid")
}

Refused if the machine user has served any logged ESB request: the ESB audit trail is append-only, so the credential can only be taken out of service with updateMachineUser(input: { enabled: false }). See Machine users.

Machine User to Endpoint Access ​

Create Machine User-Endpoint Access ​

graphql
mutation {
  createMachineUser2Endpoint(
    input: {
      machineUserID: "machine-user-uuid"
      endpointID: "endpoint-uuid"
      enabled: true
    }
  ) {
    id
    enabled
    machineUser {
      username
    }
    endpoint {
      name
    }
  }
}

Update Machine User-Endpoint Access ​

graphql
mutation {
  updateMachineUser2Endpoint(id: "relation-uuid", input: { enabled: false }) {
    id
    enabled
  }
}

Delete Machine User-Endpoint Access ​

graphql
mutation {
  deleteMachineUser2Endpoint(id: "relation-uuid")
}

Personal Access Token Management ​

Create Personal Access Token ​

graphql
mutation {
  createPersonalAccessToken(
    name: "Production API Token"
    expiresAt: "2026-12-31T23:59:59Z"
  ) {
    token
    personalAccessToken {
      id
      name
      tokenPrefix
      expiresAt
      createdAt
    }
  }
}

One-Time Token Display

The token field contains the full plaintext token and is only returned once. Store it securely.

Revoke Personal Access Token ​

graphql
mutation {
  revokePersonalAccessToken(id: "pat-uuid") {
    id
    name
    revoked
  }
}

Delete Personal Access Token ​

graphql
mutation {
  deletePersonalAccessToken(id: "pat-uuid")
}

User Access Management ​

Simplified User Management

The following mutations are the supported interface for user access management. They validate role assignments against the caller's own role.

Add User to Project ​

graphql
mutation {
  addUserToProject(
    projectID: "project-uuid"
    userID: "user-uuid"
    role: developer
  ) {
    id
    role
    user {
      externalUsername
    }
  }
}

Available Roles: readonly, tester, developer, maintainer, owner

Update User Project Role ​

graphql
mutation {
  updateUserProjectRole(id: "relation-uuid", role: readonly) {
    id
    role
  }
}

Remove User from Project ​

graphql
mutation {
  removeUserFromProject(id: "relation-uuid")
}

Add User to Tenant ​

graphql
mutation {
  addUserToTenant(tenantID: "tenant-uuid", userID: "user-uuid", roleName: "member") {
    id
    roleName
  }
}

Available Roles: tenant roles are named role definitions configured per tenant, not a fixed enum. The seeded names are member, admin and owner; list the current ones with tenantRoleDefinitions(tenantId: ...).

Update User Tenant Role ​

graphql
mutation {
  updateUserTenantRole(id: "relation-uuid", roleName: "admin") {
    id
    roleName
  }
}

Remove User from Tenant ​

graphql
mutation {
  removeUserFromTenant(id: "relation-uuid")
}

Add User to Application ​

graphql
mutation {
  addUserToApplication(
    applicationID: "app-uuid"
    userID: "user-uuid"
    role: normal
  ) {
    id
    role
  }
}

Available Roles: readonly, normal

Update User Application Role ​

graphql
mutation {
  updateUserApplicationRole(id: "relation-uuid", role: readonly) {
    id
    role
  }
}

Remove User from Application ​

graphql
mutation {
  removeUserFromApplication(id: "relation-uuid")
}

Pilet Package Management ​

Update Pilet Package ​

graphql
mutation {
  updatePiletPackage(
    projectId: "project-uuid"
    name: "@my-org/my-pilet"
    version: "1.0.0"
    description: "Updated description"
    enabled: true
  ) {
    id
    name
    version
    description
    enabled
  }
}

Pilet Upload

The actual pilet file upload happens via the REST API endpoint POST /pilets/:projectId/:name/:version. This mutation is for metadata updates only.

Enable Pilet Package ​

graphql
mutation {
  enablePiletPackage(
    projectId: "project-uuid"
    name: "@my-org/my-pilet"
    version: "1.0.0"
  ) {
    id
    enabled
  }
}

Disable Pilet Package ​

graphql
mutation {
  disablePiletPackage(
    projectId: "project-uuid"
    name: "@my-org/my-pilet"
    version: "1.0.0"
  ) {
    id
    enabled
  }
}

REST API ​

Check Machine User Auth ​

Endpoint: GET /api/machine/check

Description: Verifies machine user authentication status. Always answers 200; authenticated is false with a message when the credential did not resolve.

Authentication: Bearer Token (Machine User)

Response:

json
{
  "authenticated": true,
  "machine_user_id": "machine-user-uuid",
  "environment_id": "environment-uuid"
}

Register Backend ​

Endpoint: POST /api/machine/register-backend

Description: Registers a backend instance for trigger execution callbacks. Answers 201 for a new registration. Re-posting for the same application with the same machine user answers 200, updates the existing registration (callback URL, version, heartbeat) and rotates the callback token; keep the token from the latest response. callback_url must be absolute and must not point at a loopback, private or cloud-metadata address (400).

Authentication: Bearer Token (Machine User)

Request Body:

json
{
  "application_id": "app-uuid",
  "callback_url": "https://payment.example.com/triggers/callback",
  "version": "1.2.3"
}

Response:

json
{
  "success": true,
  "registered_id": "backend-uuid",
  "token": "callback-bearer-token",
  "message": "Backend registered successfully",
  "environment_id": "environment-uuid",
  "application_id": "app-uuid"
}

The returned token is the Bearer token the Manager uses when calling the backend's trigger callback URL.

Backend Heartbeat ​

Endpoint: PUT /api/machine/backend/:id/heartbeat

Description: Lightweight liveness signal for a registered backend (default cadence ~30 s). Only updates the registration's last_heartbeat; nothing is re-verified. A 404 means the registration is gone (Manager restarted or reaped); clients re-register and resume with the new id.

Authentication: Bearer Token (Machine User)

Response: 200 {"success": true, "registered_id": "backend-uuid", "message": "Heartbeat recorded"}; 404 if the registration does not exist; 403 if it belongs to another machine user. Error bodies are {"success": false, "message": "..."}.

Unregister Backend ​

Endpoint: DELETE /api/machine/backend/:id

Description: Graceful deregistration on shutdown. Clients treat a 404 (already gone) as success.

Authentication: Bearer Token (Machine User)

Response: 200 {"success": true, "message": "Backend unregistered"}; 404 if already gone; 403 if it belongs to another machine user.

Application Tenants ​

Endpoint: GET /api/machine/application/:id/tenants

Description: Lists the tenants that have the application enabled. Backends use it to fan scheduled work out per tenant from a single trigger delivery, and to pre-warm per-tenant config caches.

Authentication: Bearer Token (Machine User)

Response:

json
{
  "success": true,
  "tenants": [
    { "id": "tenant-uuid", "name": "acme" },
    { "id": "tenant-uuid-2", "name": "beta" }
  ]
}

The application must belong to the machine user's environment's project; otherwise 404 {"success": false, "message": "Application not found or does not belong to your project"}.

Validate Proxy Auth ​

Endpoint: POST /api/validate-proxy-auth

Description: Validates an Authorization header (PAT, or machine user Bearer/Basic credentials) on behalf of the Proxy. Internal endpoint; calls must carry the proxy shared secret.

Request Body:

json
{
  "authorization_header": "Bearer <token>",
  "application_id": "app-uuid"
}

Response:

json
{
  "valid": true,
  "user": {
    "id": "user-uuid",
    "email": "user@example.com",
    "name": "User Name",
    "username": "username",
    "is_machine_user": false,
    "is_personal_token": true
  },
  "tenant": null,
  "application": {
    "has_access": true,
    "role": "normal",
    "available_tenants": [{ "id": "tenant-uuid", "name": "Tenant Name" }]
  }
}

Validate OAuth User ​

Endpoint: POST /api/validate-oauth-user

Description: Called by the Proxy after OAuth/OIDC login to resolve the user and their application access. Internal endpoint; calls must carry the proxy shared secret.

Request Body:

json
{
  "user_id": "user-uuid",
  "email": "user@example.com",
  "name": "User Name",
  "application_id": "app-uuid"
}

Response:

json
{
  "valid": true,
  "user": {
    "id": "user-uuid",
    "email": "user@example.com",
    "name": "User Name",
    "username": "username",
    "is_machine_user": false,
    "is_personal_token": false
  },
  "tenant": { "id": "tenant-uuid", "name": "Tenant Name" }
}

CLI Login (Device Grant) ​

Endpoints: POST /api/cli/login/start, POST /api/cli/login/poll (reached through the proxied entrypoint)

Description: Browser login for pfy: a device-authorization grant ending in a Manager-minted PAT. start takes {public_key, token_name} and returns {session_id, verification_uri, user_code, interval, expires_at}; the user approves on a consent page showing the user code; poll takes {session_id} and returns {status, …} with authorization_pending / slow_down / denied / expired, or, on approval, {sealed_token, token_name, expires_at} with the PAT sealed to the CLI's ephemeral public key (fetchable once). See the CLI login docs.

Frontend Configuration ​

Endpoint: GET /api/frontend-config/:application_id/:tenant_id

Description: Retrieves the merged frontend configuration for an application as one tenant sees it (cascade project → tenant → tenant2application), including themes, locales, and enabled modules. Returns 404 if the application is not enabled for the tenant. Used by the Proxy for window.__PRODUCTIFY__ injection and polled by backends for server-side module enablement.

Tenant-less route is deprecated

The old GET /api/frontend-config/:application_id (project + application defaults only) still exists but is deprecated. Move consumers to the tenant-qualified route above.

Both routes are gated on the proxy shared secret (X-Proxy-Auth), not on a machine-user token.

Response:

json
{
  "config": {
    "enabled_modules": ["module1", "module2"],
    "enabled_themes": ["light", "dark"],
    "default_theme": "light",
    "enabled_locales": ["en", "hu"],
    "default_locale": "en",
    "key_value_pairs": [{ "key": "feature_flag", "value": "true" }]
  },
  "project_id": "project-uuid"
}

Language Pack Download ​

Endpoint: GET /language-packs/:applicationId/:locale

Description: Returns the language pack for an application's project and the given locale, with flat dot.separated keys expanded into a nested messages object. Used by the Proxy/frontend.

Response:

json
{
  "locale": "en-US",
  "messages": {
    "app": {
      "title": "Welcome"
    }
  }
}

Pilet Feed ​

Endpoint: GET /pilet-feed/:applicationId

Description: Returns the Piral-compatible pilet feed for an application. This endpoint is used by the frontend to discover and load pilets (microfrontends). Only pilets that are enabled and listed in the application's merged EnabledModules frontend configuration are included.

Authentication: Proxy shared secret (X-Proxy-Auth) only; the browser reaches it through the application vhost, which rewrites /pilet-feed to this route. The tenant-scoped form GET /pilet-feed/:applicationId/:tenantId is used by dedicated-tenant vhosts and applies that tenant's pilets.pins.

Response:

json
{
  "items": [
    {
      "name": "@my-org/my-pilet",
      "version": "1.0.0",
      "link": "/pilets/project-uuid/%40my-org%2Fmy-pilet/1.0.0",
      "hash": "abcd1234...",
      "signature": "<base64-std detached Ed25519 signature>",
      "keyId": "prod-2026-01",
      "spec": "v2"
    }
  ]
}

hash is the lowercase hex SHA-256 of the exact bundle bytes (no sha256- prefix). signature is a base64-std detached Ed25519 signature over those same bytes, and keyId names the key to resolve from the injected window.__PRODUCTIFY__.config.pilet_signing_keys trust anchor. The browser loader verifies both before executing the bundle; a legacy unsigned package omits signature/keyId and is refused.

link is root-relative and is fetched same-origin. Every segment is percent-encoded, so a scoped name such as @my-org/my-pilet appears as %40my-org%2Fmy-pilet; fetch it as given rather than building it from the name.

Pilet Package Management ​

List Pilets ​

Endpoint: GET /pilets/:projectId

Description: Lists all pilet packages for a project.

Authentication: Proxy shared secret, then a PAT or a proxy-forwarded browser session, then project membership. Upload and delete need a project role above readonly. A PAT sent through a rendered Manager vhost does not reach these routes; see Pilet API.

Query Parameters:

  • enabled (optional): Filter by enabled status (true/false)

Response:

json
{
  "packages": [
    {
      "id": "pilet-uuid",
      "name": "@my-org/my-pilet",
      "version": "1.0.0",
      "description": "My pilet description",
      "size": 123456,
      "hash": "abcd1234...",
      "enabled": true,
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-02T00:00:00Z"
    }
  ],
  "count": 1
}

Upload Pilet ​

Endpoint: POST /pilets/:projectId/:name/:version

Description: Uploads a new pilet package. A (project, name, version) that already exists answers 409; versions are immutable. The upload is recorded in the project audit log.

Authentication: As for List Pilets, with a project role above readonly

Request:

  • Content-Type: multipart/form-data
  • File field: file (the pilet package, e.g. .tgz or .js)
  • Optional query parameter: description (string)

Response:

json
{
  "id": "pilet-uuid",
  "name": "@my-org/my-pilet",
  "version": "1.0.0",
  "size": 123456,
  "hash": "abcd1234...",
  "message": "Package uploaded successfully"
}

Download Pilet (management) ​

Endpoint: GET /pilets/:projectId/:name/:version

Description: Downloads a pilet package file for an operator or CI job. Not the browser path; see Download Pilet Bundle below.

Authentication: As for List Pilets

Response: Binary package content

Headers:

  • Content-Type: the package's stored content type (e.g. application/gzip or application/javascript)
  • Content-Length: package size in bytes
  • X-Pilet-Hash: SHA256 hash of the package content
  • Cache-Control: public, max-age=31536000, immutable

Download Pilet Bundle (browser) ​

Endpoint: GET /pilet-bundle/:applicationId/:projectId/:name/:version

Description: Serves a pilet bundle to an end user's browser. This is the route a rendered application vhost forwards /pilets/* to (uri strip_prefix /pilets + rewrite /pilet-bundle/<applicationId>{uri}), so the URL the browser requests, and the link the feed emits, remains /pilets/{projectId}/{name}/{version}.

Authentication: None. Proxy shared secret only, the same trust tier as the pilet feed.

Scoping is structural rather than credential-based: the application id is written by the vhost renderer, the client-supplied project id is refused unless it is that application's own project, and the same enablement filter as the feed applies, so the route can only hand out bytes the feed for that application already offers. Every rejection answers a single 404 Package not found.

Response: Binary package content, with the same headers as the management download.

Delete Pilet ​

Endpoint: DELETE /pilets/:projectId/:name/:version

Description: Soft-deletes a pilet package by disabling it. Recorded in the project audit log.

Authentication: As for List Pilets, with a project role above readonly

Response:

json
{
  "message": "Package disabled successfully"
}

ESB (Enterprise Service Bus) Endpoints ​

Endpoint: ANY /esb/:applicationId/*endpointPath

Description: Routes requests to configured endpoints through the ESB. Supports all HTTP methods (GET, POST, PUT, DELETE, etc.). The endpoint is selected by matching the request path (the part after the application ID) against the endpoint's consumeURL (exact match), and the machine user must have an enabled access relation (MachineUser2Endpoint) to that endpoint. If a transformation template is configured, the request body is transformed before forwarding, and the call is recorded in the ESB audit log according to the endpoint's logLevel.

Authentication: Bearer Token (Machine User)

Example:

bash
# Request to ESB
GET /esb/app-uuid/api/users
Authorization: Bearer <machine-user-token>

# Routed to the enabled endpoint of the application
# whose consumeURL exactly matches "/api/users"

Error responses: 404 if no enabled endpoint matches, 403 if the machine user has no access to the endpoint, 502 if forwarding to the target fails.


Other Manager routes ​

These are served by the Manager but are not application-integration surfaces; they are called by the proxy, by the platform's own operators, or by the CLI. Full details in the REST route map.

RouteAuthPurpose
GET /api/proxy/comms/:appID/notificationsproxy secret + injected identityRecipient inbox, newest-first, keyset-paginated.
GET /api/proxy/comms/:appID/unreadproxy secret + injected identityUnread probe for the bell (an EXISTS, not a count).
POST /api/proxy/comms/:appID/notifications/readproxy secret + injected identityMark read by ids or all.
POST /api/proxy/comms/:appID/notifications/seenproxy secret + injected identityMark seen by ids only.
GET /api/proxy/comms/:appID/wsproxy secret + injected identityComms websocket upgrade.
POST /api/proxy/wakeproxy secretScale-from-zero: raise a deployment's replica floor. 202, or 404/409/502.
GET /api/proxy/readyproxy secretBackend readiness for app_id + env.
GET /api/proxy/deploymentproxy secretScale-to-zero eligibility + healthy backend count.
GET /api/proxy/user/:user_id/tenantsproxy secretTenant catalog for the proxy's selector.
GET /pilet-keys/:applicationIdproxy secretPilet signing public keys, keyed by key id. Development and debugging only, not the frontend trust anchor.
GET/POST /cli/authorizeproxy secret + user identityBrowser consent page for the CLI device grant; approving mints and seals a PAT.
GET /api/audit/exportproxy secret + PAT + system adminSIEM audit export.
GET /api/audit/evidenceproxy secret + PAT + system adminRegistered compliance-evidence query names.
GET /api/audit/evidence/:nameproxy secret + PAT + system adminRun one evidence query over an RFC3339 from/to window; returns rows plus audit-chain status.
GET /api/access/reviewproxy secret + PAT + system adminAccess-review report.
POST /api/access/review/attestproxy secret + PAT + system adminRecord an access-review attestation.

Error Handling ​

GraphQL Errors ​

GraphQL errors follow the standard GraphQL error structure:

json
{
  "errors": [
    {
      "message": "ent: settings not found",
      "path": ["settings"]
    }
  ],
  "data": null
}

REST API Errors ​

Machine-user routes (/api/machine/*, /esb/*) and handler-level failures return HTTP status codes with an error message:

json
{
  "error": "Invalid credentials"
}

Proxy-gated routes answer 401 with a diagnostic body

/query, /pilets/*, /api/audit/*, /api/access/*, /cli/authorize and the whole /api/proxy/*, /api/frontend-config/*, /api/validate-*, /api/cli/login/*, /language-packs/*, /pilet-feed/*, /pilet-keys/* surface sit behind the proxy shared-secret gate. A rejection is 401 {"error": "..."} whose text says whether the Manager has no PFY_PROXY_SECRET configured, the X-Proxy-Auth header was missing or wrong, or the identity headers were missing. Nothing is written to the Manager log. See Error Response Format.

Common HTTP Status Codes:

  • 200 - Success
  • 201 - Created (first backend registration)
  • 202 - Accepted (comms emit/publish)
  • 400 - Bad Request
  • 401 - Unauthorized
  • 403 - Forbidden
  • 404 - Not Found
  • 429 - Too Many Requests (machine-auth throttle, CLI login polling, comms rate limit; honour Retry-After)
  • 500 - Internal Server Error

Rate Limiting ​

Three places are rate limited:

  • The credential-validation endpoints (/api/validate-*) lock a client out temporarily after repeated failures.
  • /api/machine/* and /esb/* throttle repeated failed authentications per client and answer 429 with Retry-After.
  • /api/machine/comms/* limits emits and publishes per application (429 {"error":"rate_limited"}).

There is no general limit on the GraphQL API; for production deployments, add rate limiting at the reverse proxy (Caddy or nginx).


Best Practices ​

1. Use Filtering and Pagination ​

Always use pagination for list queries to avoid performance issues:

graphql
query {
  projects(
    filters: [{ field: "name", value: "prod" }]
    order: { field: "createdAt", direction: DESC }
    pagination: { limit: 20, offset: 0 }
  ) {
    id
    name
  }
}

2. Request Only Required Fields ​

GraphQL allows you to request only the fields you need:

graphql
query {
  project(slug: "my-project") {
    id
    name
    # Don't fetch relations unless needed
  }
}

3. Secure Token Storage ​

  • Never commit tokens to version control
  • Use environment variables or secret management systems
  • Rotate tokens regularly
  • Use expiration dates for PATs

4. Handle Errors Gracefully ​

Always check for errors in GraphQL responses:

javascript
const response = await fetch("/query", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token}`,
  },
  body: JSON.stringify({ query, variables }),
});

const { data, errors } = await response.json();

if (errors) {
  // Handle errors
  console.error("GraphQL errors:", errors);
}

5. Use Machine Users for Services ​

For backend services and integrations, always use machine users instead of personal access tokens. Machine users provide:

  • Environment-scoped access (one credential serves every tenant the application is enabled for)
  • No expiration (unless disabled)
  • Better audit trails
  • Service account semantics

Examples ​

Complete Project Setup ​

Run these one at a time (a GraphQL document may contain only one anonymous operation).

graphql
# 1. Create project
mutation {
  createProject(input: { name: "E-commerce Platform", slug: "ecommerce" }) {
    id
  }
}
graphql
# 2. Create tenant
mutation {
  createTenant(
    projectId: "project-uuid"
    input: { name: "Store A", slug: "store-a" }
  ) {
    id
  }
}
graphql
# 3. Create application (applications belong to the project)
mutation {
  createApplication(
    projectId: "project-uuid"
    input: {
      name: "Storefront"
      slug: "storefront"
      description: "Customer-facing web application"
    }
  ) {
    id
  }
}
graphql
# 4. Configure settings
mutation {
  updateFrontendModuleConfig(
    id: "settings-uuid"
    input: {
      EnabledThemes: ["light", "dark"]
      DefaultTheme: "light"
      DefaultLocale: "en"
    }
  ) {
    id
  }
}

Trigger-Based Workflow ​

graphql
# 1. Create trigger
mutation {
  createTrigger(
    applicationId: "app-uuid"
    input: {
      name: "Hourly Order Processing"
      description: "Processes orders hourly"
      cronExpression: "0 * * * *"
      runKey: "hourly-orders"
      enabled: true
    }
  ) {
    id
  }
}
http
# 2. Register the backend for callbacks (REST, machine-user token)
POST /api/machine/register-backend
Authorization: Bearer <machine-user-token>
Content-Type: application/json

{
  "application_id": "app-uuid",
  "callback_url": "https://backend.example.com/triggers/callback"
}

The Manager now POSTs {"trigger_id", "trigger_name", "cron_expression", "run_key": "hourly-orders", "executed_at"} to the callback URL every hour, with the registration's token as a Bearer credential. ESB endpoints are unrelated to triggers; see ESB for those.


API Versioning ​

The GraphQL API is unversioned but follows these principles:

  • New fields and types are added without breaking existing queries.
  • Fields are marked deprecated before removal (promoteApplication and copyApplication are the current examples).
  • Existing fields keep their contracts.

REST endpoints are currently unversioned.