// wiki / graphql

GraphQL API

One endpoint, one request, exactly the fields you asked for. Everything you can do over HTTP & REST — creating flags, configuring environments, targeting, and evaluating — you can do over GraphQL, with the same Management Key and the same permission checks. Nothing new to install and nothing extra to sign up for.

Download the GraphQL Postman collection

Eleven ready-to-run requests covering the whole API over GraphQL, each with response tests, plus an environment template whose values are left blank for you to fill in. Connecting to Maxlona takes one paste of your Management Key.

Download Maxlona GraphQL Collection
In the zip
  • Maxlona GraphQL Consumer — the collection.
  • Maxlona GraphQL — the environment template, with empty values.
What it runs
  • 01 — read and evaluate an existing flag. Changes nothing.
  • 02 — create, configure, toggle, evaluate, delete a temporary flag.
  • 03 — security checks that are meant to fail.

Connect it to your account

  1. Unzip, then in Postman choose File → Import and import both JSON files.
  2. Pick Maxlona GraphQL in the environment dropdown, top right.
  3. Create a Management Key under Management Keys with CanRead, CanWrite and CanEvaluate, scoped to the application you want to work in. The key is shown once, so copy it straight away.
  4. Paste it into the managementKey variable's Current Value column, not Initial Value — current values stay on your machine and never travel with an export or a shared workspace.
  5. Fill in application, environment, flagName and userId with your own values. Leave runGraphQLFlagName empty; the collection sets it while it runs.
  6. Run folder 01 first to confirm the key works, then 02 top to bottom.
Keep the key out of version control. The template ships empty on purpose. Once you have pasted a key in, treat the environment as a secret: don't export it, commit it, or share the workspace it lives in.

Prefer plain REST? The Maxlona HTTP Consumer collection ↓ covers the same API over HTTP, and includes these GraphQL requests as folder 04.

The endpoint

Every operation is an HTTP POST to a single URL, with the same x-management-key header the REST API uses. Send Accept: application/json and the response is ordinary JSON.

POST /api/graphql
x-management-key: YOUR-MANAGEMENT-KEY
Content-Type: application/json
Accept: application/json

Curl

curl -X POST $HOST/api/graphql \
  -H "x-management-key: $FF_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"query":"{ flags { name project type isArchived } }"}'
Same key, same rules. A field needs the same permission its REST endpoint does: CanRead for queries, CanWrite for mutations, CanEvaluate for evaluate. Application and environment scope, the subscription check, and the per-key rate limit all apply exactly as they do over REST — GraphQL is a different way in, not a way around.
Requests must be POST, and queries are read from the request body rather than the URL. Nesting is limited to a sensible depth, and the number of evaluations in a single request is capped, so one document can't be used to multiply past your rate limit.

What you can call

The field names mirror the REST endpoints one for one, so anything documented on HTTP & REST applies here too, including the request body shapes.

FieldDoes whatPermission
flagsEvery flag the key can see.CanRead
flagOne flag by name.CanRead
flagConfigsStage configurations, optionally for one stage.CanRead
flagEnvironmentsWhich environments a flag is enabled for.CanRead
environments, applicationsYour organization's stages and applications.CanRead
killSwitchActive kill-switch rules.CanRead
evaluateResolve a flag for one user, with an optional reason.CanEvaluate
createFlag, updateFlagCreate or edit a flag's metadata.CanWrite
archiveFlag, restoreFlag, deleteFlagRetire, bring back, or remove a flag.CanWrite
upsertFlagConfig, setFlagConfigEnabled, deleteFlagConfigConfigure targeting and rollout for a stage, or switch it on and off.CanWrite
updateFlagEnvironmentsChoose which environments the flag applies to.CanWrite
replaceEnvironments, renameEnvironmentManage the organization's stages.CanWrite
replaceApplications, renameApplicationManage the organization's applications.CanWrite
replaceKillSwitchSet or clear kill-switch rules.CanWrite

Mutation fields run one after another in the order you write them, so a create followed by a configure in the same document behaves the way you'd expect.

Reading flags

Ask for the fields you need and nothing else.

query ListFlags {
  flags {
    name
    project
    type
    isArchived
    variants { key value valueType }
  }
}

One round trip can answer more than one question — here, a flag and its configurations together, where REST would need two calls:

query OneFlag($name: String!) {
  flag(name: $name) {
    name
    project
    description
    tags
  }
  flagConfigs(name: $name) {
    stage
    clientId
    enabled
    defaultVariantKey
    rolloutPercentage
  }
}

Creating and configuring

Mutation

mutation CreateFlag($input: CreateFlagInput!) {
  createFlag(input: $input) {
    name
    project
    type
    createdAt
  }
}

Variables

{
  "input": {
    "name": "checkout-redesign",
    "project": "Main App",
    "type": "release",
    "description": "Q3 checkout flow rewrite",
    "tags": ["checkout", "q3"],
    "variants": [
      { "key": "on",  "value": true,  "valueType": "boolean" },
      { "key": "off", "value": false, "valueType": "boolean" }
    ]
  }
}

Targeting for one stage

mutation Configure($name: String!, $input: UpsertFlagConfigInput!) {
  upsertFlagConfig(name: $name, input: $input) {
    stage
    enabled
    rolloutPercentage
  }
}
{
  "name": "checkout-redesign",
  "input": {
    "stage": "production",
    "enabled": true,
    "defaultVariantKey": "off",
    "rolloutPercentage": 100,
    "rules": [
      {
        "id": "pro-users",
        "priority": 10,
        "conditions": [
          { "field": "plan", "operator": "==", "value": "pro" }
        ],
        "allocations": [
          { "variantKey": "on", "percentage": 100 }
        ]
      }
    ]
  }
}
A rule condition's field is a JSON property path into your evaluation context: letters, digits, underscores and dots. A name with a hyphen or a space is rejected, so use lenderId, not Lender-id.

Evaluating

The same evaluator that answers the REST call, reached a different way. Ask for reason and you get the explanation back — there is no separate explain switch, because requesting the field is the switch.

query Evaluate($key: String!, $userId: String!, $stage: String!, $context: Any) {
  evaluate(key: $key, userId: $userId, stage: $stage, context: $context) {
    key
    variant
    reason { code message ruleId }
  }
}
{
  "key": "checkout-redesign",
  "userId": "user-42",
  "stage": "production",
  "context": { "plan": "pro", "country": "DE" }
}

Response

{
  "data": {
    "evaluate": {
      "key": "on",
      "variant": true,
      "reason": {
        "code": "rule_matched",
        "message": "Matched rule 'pro-users' (priority 10). Variant chosen by allocation.",
        "ruleId": "pro-users"
      }
    }
  }
}

Several users in one request

Give each evaluation an alias and they come back side by side under those names — useful for rendering a whole screen's worth of flags, or for comparing what two audiences would see.

query EvaluateBoth($key: String!, $stage: String!, $pro: Any, $free: Any) {
  proUser: evaluate(key: $key, userId: "user-42", stage: $stage, context: $pro) {
    key
    variant
  }
  freeUser: evaluate(key: $key, userId: "user-77", stage: $stage, context: $free) {
    key
    variant
  }
}

How failures come back

This is the one real difference from REST. A request that reached the API answers 200 even when the operation failed, and the failure is described in errors. So check errors, not the status code.

{
  "errors": [
    {
      "message": "Flag not found.",
      "path": ["flag"],
      "extensions": { "code": "NOT_FOUND", "status": 404 }
    }
  ],
  "data": { "flag": null }
}

Each entry carries an extensions.code and the status the REST call would have returned:

CodeStatusMeaning
BAD_REQUEST400The input failed validation.
FORBIDDEN403The key lacks the permission or the application/environment scope.
NOT_FOUND404No such flag, configuration, or resource.
CONFLICT409The change clashes with existing state, such as a dependency.
AUTH_NOT_AUTHORIZED—The key is valid but cannot use that field.

Three cases still answer with an HTTP status instead, before the query is ever read: a missing or unknown key returns 401, an inactive subscription returns 402, and going over your rate limit returns 429. See rate limits and errors.

GraphQL and REST are the same API with the same credentials, so you can mix them freely — evaluate over REST from a hot path, manage flags over GraphQL from a build script, using one key for both.