GraphQL Integration
The GraphQL API at POST /query is how you manage everything in the Manager from code: projects, tenants, applications, endpoints, triggers, settings, users and tokens. It is the same API the Manager UI and the pfy CLI use. Below: authentication, a minimal client in JavaScript, Python and Go, and a few patterns that avoid common mistakes. The full list of operations is in the API Reference.
Complete API Reference
See the API Reference for complete GraphQL schema documentation, all queries, mutations, and detailed examples.
Quick Start
Endpoint
POST http://your-manager-host:8080/queryGraphQL Playground
Interactive schema explorer (development mode only):
GET http://localhost:8080/gqlAuthentication
Include authentication token in request headers:
Authorization: Bearer <your-token>Use either:
- a Personal Access Token, sent as a Bearer token, for CLI tools and scripts
- an OAuth session, when requests come through the Productify proxy
Machine user tokens are not accepted on the GraphQL endpoint; they are limited to the REST (/api/machine/*) and ESB endpoints.
See Authentication for details.
Send /query through the proxy, not straight at port 8080
/query is gated on the proxy shared secret before your PAT is even read, so a request that reaches the Manager's port directly is rejected with a 401 whose JSON body says the request did not come through the trusted proxy, no matter how good the token is. Point clients at the proxy-fronted host (https://manager.example.com/query), which validates the PAT and adds the X-Proxy-Auth and identity headers for you.
The localhost:8080 examples below therefore only work against a local development Manager running with PFY_ENV=development and PFY_DEV_USER_OVERRIDE_ENABLED=true, which is the one supported way to run without a proxy in front. See Configuration.
Client Setup
JavaScript/TypeScript
const graphqlClient = async (query, variables = {}) => {
const response = await fetch("http://localhost:8080/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.MANAGER_TOKEN}`,
},
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await response.json();
if (errors) {
throw new Error(errors[0].message);
}
return data;
};Python
import requests
import os
def graphql_client(query, variables=None):
response = requests.post(
'http://localhost:8080/query',
json={'query': query, 'variables': variables or {}},
headers={
'Authorization': f'Bearer {os.getenv("MANAGER_TOKEN")}'
}
)
result = response.json()
if 'errors' in result:
raise Exception(result['errors'][0]['message'])
return result['data']Go
package main
import (
"bytes"
"encoding/json"
"net/http"
"os"
)
type GraphQLRequest struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"`
}
func GraphQLClient(query string, variables map[string]interface{}) (map[string]interface{}, error) {
reqBody, _ := json.Marshal(GraphQLRequest{
Query: query,
Variables: variables,
})
req, _ := http.NewRequest("POST", "http://localhost:8080/query", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("MANAGER_TOKEN"))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}Common Patterns
Query with Variables
const query = `
query GetTenant($projectId: ID!, $slug: String!) {
tenant(projectId: $projectId, slug: $slug) {
id
name
project {
applications {
id
name
}
}
}
}
`;
const data = await graphqlClient(query, {
projectId: "project-123",
slug: "my-tenant",
});Create Resource
Applications belong to a project, not to a tenant: you create one against a project and then enable it per tenant. See Multi-tenancy.
const mutation = `
mutation CreateApplication($projectId: ID!, $input: CreateApplicationInput!) {
createApplication(projectId: $projectId, input: $input) {
id
name
slug
}
}
`;
const result = await graphqlClient(mutation, {
projectId: "project-uuid",
input: {
name: "My App",
slug: "my-app",
description: "Application description",
},
});Error Handling
try {
const data = await graphqlClient(query, variables);
console.log("Success:", data);
} catch (error) {
console.error("GraphQL Error:", error.message);
// Error messages are plain text, e.g. "ent: tenant not found" or
// "unauthorized: ...". Match on the message when you need to branch.
if (error.message.includes("not found")) {
// Resource not found
} else if (error.message.includes("unauthorized")) {
// The caller may not do this
}
}An HTTP 401 (not a GraphQL error) means the request never reached the API: the PAT was rejected or the request bypassed the proxy. The body is JSON with an error field that names the reason.
Best Practices
Use Variables
Always use variables instead of hardcoding values:
// [NO] Avoid hardcoding
const query = `
query {
tenant(projectId: "abc-123", slug: "my-tenant") { name }
}
`;
// [OK] Use variables
const query = `
query GetTenant($projectId: ID!, $slug: String!) {
tenant(projectId: $projectId, slug: $slug) { name }
}
`;
const data = await graphqlClient(query, {
projectId: "abc-123",
slug: "my-tenant",
});Request Only Needed Fields
GraphQL allows precise field selection - use it:
// [NO] Over-fetching
const query = `
query {
applications(projectSlug: "my-project", filters: [], order: {...}, pagination: {...}) {
id name slug description createdAt updatedAt
project { id name slug createdAt updatedAt }
endpoints { id name url enabled createdAt }
triggers { id name cronExpression enabled }
}
}
`;
// [OK] Request only what you need
const query = `
query {
applications(projectSlug: "my-project", filters: [], order: {...}, pagination: {...}) {
id
name
}
}
`;Use Fragments for Reusability
const fragments = `
fragment ApplicationFields on Application {
id
name
slug
}
`;
const query = `
${fragments}
query GetApplications($projectSlug: String!) {
applications(projectSlug: $projectSlug, filters: [], order: {...}, pagination: {...}) {
...ApplicationFields
}
}
`;Handle Pagination
async function getAllApplications(projectSlug) {
const allApps = [];
let offset = 0;
const limit = 50; // must be one of 1, 10, 20, 25, 50, 100
while (true) {
const data = await graphqlClient(
`
query GetApplications($projectSlug: String!, $limit: Int!, $offset: Int!) {
applications(
projectSlug: $projectSlug
filters: []
order: { field: "name", direction: ASC }
pagination: { limit: $limit, offset: $offset }
) {
id
name
}
}
`,
{ projectSlug, limit, offset }
);
if (data.applications.length === 0) break;
allApps.push(...data.applications);
offset += limit;
}
return allApps;
}GraphQL Tools
Apollo Client (JavaScript)
npm install @apollo/client graphqlimport { ApolloClient, InMemoryCache, gql } from "@apollo/client";
const client = new ApolloClient({
uri: "http://localhost:8080/query",
cache: new InMemoryCache(),
headers: {
authorization: `Bearer ${process.env.MANAGER_TOKEN}`,
},
});
const { data } = await client.query({
query: gql`
query GetTenants($projectSlug: String!) {
tenants(projectSlug: $projectSlug, filters: [], order: {...}, pagination: {...}) {
id
name
}
}
`,
variables: { projectSlug: "my-project" },
});urql (React)
npm install urql graphqlimport { createClient, useQuery } from "urql";
const client = createClient({
url: "http://localhost:8080/query",
fetchOptions: {
headers: {
authorization: `Bearer ${process.env.MANAGER_TOKEN}`,
},
},
});
function TenantsList() {
const [result] = useQuery({
query: `
query GetTenants($projectSlug: String!) {
tenants(projectSlug: $projectSlug, filters: [], order: {...}, pagination: {...}) {
id name
}
}
`,
variables: { projectSlug: "my-project" },
});
return <div>{/* render */}</div>;
}See Also
- API Reference - Complete GraphQL schema documentation
- Authentication - Authentication methods
- REST API - The routes that are not GraphQL, and the proxy trust boundary