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 thepfyCLI 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/readyzgives
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:
Authorization: Bearer <your-token>Creating a PAT:
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:
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:
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
input Filter {
field: String!
value: String!
}Order
input Order {
field: String!
direction: OrderDirection!
}
enum OrderDirection {
ASC
DESC
}Pagination
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:
enum EndpointAuthType {
Basic # HTTP Basic Authentication
Bearer # Bearer Token Authentication
None # No authentication required
}Message Types:
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:
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
query {
projects(
filters: []
order: { field: "createdAt", direction: DESC }
pagination: { limit: 10, offset: 0 }
) {
id
name
slug
createdAt
}
}Get Project by Slug
query {
project(slug: "my-project") {
id
name
slug
tenants {
id
name
}
applications {
id
name
}
}
}Tenants
List Tenants
query {
tenants(
projectSlug: "my-project"
filters: []
order: { field: "name", direction: ASC }
pagination: { limit: 20, offset: 0 }
) {
id
name
slug
deploymentMode
}
}Get Tenant
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
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
query {
application(projectId: "project-uuid", slug: "app-slug") {
id
name
slug
description
triggers {
id
name
cronExpression
enabled
}
}
}Settings
Get Settings
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
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
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
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
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
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)
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
query {
me {
id
externalName
externalUsername
externalEmail
role
}
}List Users
query {
users(
filters: []
order: { field: "externalName", direction: ASC }
pagination: { limit: 20, offset: 0 }
) {
id
externalName
externalUsername
externalEmail
role
}
}Search Provider Users
query {
userProviderSearch(name: "john") {
id
username
email
displayName
disabled
}
}User Relationships
List User-Application Relations
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
query {
user2application(application: "app-uuid", id: "relation-uuid") {
id
role
user {
externalUsername
}
}
}List User-Project Relations
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
query {
user2project(project: "project-uuid", id: "relation-uuid") {
id
role
user {
externalUsername
}
}
}List User-Tenant Relations
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
query {
user2tenant(tenant: "tenant-uuid", id: "relation-uuid") {
id
roleName
user {
externalUsername
}
}
}Machine User to Endpoint Relations
List Machine User-Endpoint Relations
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
query {
machineUser2endpoint(endpoint: "endpoint-uuid", id: "relation-uuid") {
id
enabled
}
}Audit Logs
List Audit Logs
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
query {
piletPackages(projectId: "project-uuid", enabled: true) {
packages {
id
name
version
description
size
hash
enabled
createdAt
updatedAt
}
count
}
}Get Pilet Package
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
mutation {
createProject(input: { name: "My Project", slug: "my-project" }) {
id
name
slug
}
}Update Project
mutation {
updateProject(
id: "project-uuid"
input: { name: "Updated Project Name", slug: "updated-project" }
) {
id
name
slug
}
}Delete Project
mutation {
deleteProject(id: "project-uuid")
}Tenant Management
Create Tenant
mutation {
createTenant(
projectId: "project-uuid"
input: { name: "Customer A", slug: "customer-a" }
) {
id
name
slug
deploymentMode
}
}Update Tenant
mutation {
updateTenant(id: "tenant-uuid", input: { name: "Updated Customer Name" }) {
id
name
}
}Delete Tenant
mutation {
deleteTenant(id: "tenant-uuid")
}Application Management
Create Application
mutation {
createApplication(
projectId: "project-uuid"
input: {
name: "Web App"
slug: "web-app"
description: "Main web application"
}
) {
id
name
slug
}
}Update Application
mutation {
updateApplication(
id: "app-uuid"
input: { description: "Updated description" }
) {
id
description
}
}Delete Application
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:
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
mutation {
deleteBackendSettings(id: "backend-settings-uuid")
}Delete Frontend Settings
mutation {
deleteFrontendSettings(id: "frontend-settings-uuid")
}Update Backend Module Configuration
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
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
mutation {
createLanguagePack(projectId: "project-uuid", input: { name: "en-US" }) {
id
name
}
}Update Language Pack Content
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
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/esbrequests are matched against it exactlyurl- The absolutehttp://orhttps://backend URL the ESB forwards to; a request to an endpoint without one answers502authType- Authentication method:Basic,Bearer, orNone(default:None)inputMessageType- Expected input format:JSON,XML, orSOAP(optional)outputMessageType- Output format after transformation:JSON,XML, orSOAP(optional)logLevel- ESB logging verbosity:NONE,BASIC, orFULL(default:BASIC)
Update Endpoint
mutation {
updateEndpoint(
id: "endpoint-uuid"
input: {
enabled: false
url: "https://new-backend.example.com/webhook"
logLevel: FULL
}
) {
id
enabled
url
logLevel
}
}Delete Endpoint
mutation {
deleteEndpoint(id: "endpoint-uuid")
}Trigger Management
Create Trigger
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
mutation {
updateTrigger(
id: "trigger-uuid"
input: { enabled: false, cronExpression: "0 2 * * *" }
) {
id
enabled
cronExpression
}
}Machine User Management
Create Machine User with Token
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
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
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
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
mutation {
createMachineUser2Endpoint(
input: {
machineUserID: "machine-user-uuid"
endpointID: "endpoint-uuid"
enabled: true
}
) {
id
enabled
machineUser {
username
}
endpoint {
name
}
}
}Update Machine User-Endpoint Access
mutation {
updateMachineUser2Endpoint(id: "relation-uuid", input: { enabled: false }) {
id
enabled
}
}Delete Machine User-Endpoint Access
mutation {
deleteMachineUser2Endpoint(id: "relation-uuid")
}Personal Access Token Management
Create Personal Access Token
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
mutation {
revokePersonalAccessToken(id: "pat-uuid") {
id
name
revoked
}
}Delete Personal Access Token
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
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
mutation {
updateUserProjectRole(id: "relation-uuid", role: readonly) {
id
role
}
}Remove User from Project
mutation {
removeUserFromProject(id: "relation-uuid")
}Add User to Tenant
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
mutation {
updateUserTenantRole(id: "relation-uuid", roleName: "admin") {
id
roleName
}
}Remove User from Tenant
mutation {
removeUserFromTenant(id: "relation-uuid")
}Add User to Application
mutation {
addUserToApplication(
applicationID: "app-uuid"
userID: "user-uuid"
role: normal
) {
id
role
}
}Available Roles: readonly, normal
Update User Application Role
mutation {
updateUserApplicationRole(id: "relation-uuid", role: readonly) {
id
role
}
}Remove User from Application
mutation {
removeUserFromApplication(id: "relation-uuid")
}Pilet Package Management
Update Pilet Package
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
mutation {
enablePiletPackage(
projectId: "project-uuid"
name: "@my-org/my-pilet"
version: "1.0.0"
) {
id
enabled
}
}Disable Pilet Package
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:
{
"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:
{
"application_id": "app-uuid",
"callback_url": "https://payment.example.com/triggers/callback",
"version": "1.2.3"
}Response:
{
"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:
{
"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:
{
"authorization_header": "Bearer <token>",
"application_id": "app-uuid"
}Response:
{
"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:
{
"user_id": "user-uuid",
"email": "user@example.com",
"name": "User Name",
"application_id": "app-uuid"
}Response:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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..tgzor.js) - Optional query parameter:
description(string)
Response:
{
"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/gziporapplication/javascript)Content-Length: package size in bytesX-Pilet-Hash: SHA256 hash of the package contentCache-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:
{
"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:
# 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.
| Route | Auth | Purpose |
|---|---|---|
GET /api/proxy/comms/:appID/notifications | proxy secret + injected identity | Recipient inbox, newest-first, keyset-paginated. |
GET /api/proxy/comms/:appID/unread | proxy secret + injected identity | Unread probe for the bell (an EXISTS, not a count). |
POST /api/proxy/comms/:appID/notifications/read | proxy secret + injected identity | Mark read by ids or all. |
POST /api/proxy/comms/:appID/notifications/seen | proxy secret + injected identity | Mark seen by ids only. |
GET /api/proxy/comms/:appID/ws | proxy secret + injected identity | Comms websocket upgrade. |
POST /api/proxy/wake | proxy secret | Scale-from-zero: raise a deployment's replica floor. 202, or 404/409/502. |
GET /api/proxy/ready | proxy secret | Backend readiness for app_id + env. |
GET /api/proxy/deployment | proxy secret | Scale-to-zero eligibility + healthy backend count. |
GET /api/proxy/user/:user_id/tenants | proxy secret | Tenant catalog for the proxy's selector. |
GET /pilet-keys/:applicationId | proxy secret | Pilet signing public keys, keyed by key id. Development and debugging only, not the frontend trust anchor. |
GET/POST /cli/authorize | proxy secret + user identity | Browser consent page for the CLI device grant; approving mints and seals a PAT. |
GET /api/audit/export | proxy secret + PAT + system admin | SIEM audit export. |
GET /api/audit/evidence | proxy secret + PAT + system admin | Registered compliance-evidence query names. |
GET /api/audit/evidence/:name | proxy secret + PAT + system admin | Run one evidence query over an RFC3339 from/to window; returns rows plus audit-chain status. |
GET /api/access/review | proxy secret + PAT + system admin | Access-review report. |
POST /api/access/review/attest | proxy secret + PAT + system admin | Record an access-review attestation. |
Error Handling
GraphQL Errors
GraphQL errors follow the standard GraphQL error structure:
{
"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:
{
"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- Success201- Created (first backend registration)202- Accepted (comms emit/publish)400- Bad Request401- Unauthorized403- Forbidden404- Not Found429- Too Many Requests (machine-auth throttle, CLI login polling, comms rate limit; honourRetry-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 answer429withRetry-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:
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:
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:
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).
# 1. Create project
mutation {
createProject(input: { name: "E-commerce Platform", slug: "ecommerce" }) {
id
}
}# 2. Create tenant
mutation {
createTenant(
projectId: "project-uuid"
input: { name: "Store A", slug: "store-a" }
) {
id
}
}# 3. Create application (applications belong to the project)
mutation {
createApplication(
projectId: "project-uuid"
input: {
name: "Storefront"
slug: "storefront"
description: "Customer-facing web application"
}
) {
id
}
}# 4. Configure settings
mutation {
updateFrontendModuleConfig(
id: "settings-uuid"
input: {
EnabledThemes: ["light", "dark"]
DefaultTheme: "light"
DefaultLocale: "en"
}
) {
id
}
}Trigger-Based Workflow
# 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
}
}# 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 (
promoteApplicationandcopyApplicationare the current examples). - Existing fields keep their contracts.
REST endpoints are currently unversioned.