> For the complete documentation index, see [llms.txt](https://docs.getantidote.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.getantidote.com/welcome-to-antidote/integrations/graphql-api.md).

# GraphQL API

Query Antidote data and trigger runs programmatically.

The Duku GraphQL API gives you programmatic access to products, targets, and runs.

### Next steps

For a complete CI walkthrough, see Triggering explorations from any CI. It covers token exchange, preview and environment flows, and pipeline gating.

### Endpoint

```
POST https://platform.duku.ai/graphql
```

### Authentication

Every request requires a Bearer token in the `Authorization` header. Use the OAuth2 client credentials flow described in API Keys.

### Request format

All requests use an HTTP `POST` with a JSON body:

```json
{
  "query": "query { subjects { id name } }",
  "variables": {}
}
```

Example request:

```bash
curl -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ subjects { id name } }"}'
```

{% hint style="info" %}
Example IDs such as `sms_...` (products), `smt_...` (targets), and `key_...` are placeholders.
{% endhint %}

### Queries

* [Issues API](/welcome-to-antidote/integrations/issues-api.md)

#### List products

```graphql
query {
  subjects {
    id
    name
  }
}
```

#### List targets

```graphql
query GetTargets($subjectId: ID) {
  targets(subjectId: $subjectId) {
    id
    name
  }
}
```

#### List runs for a target

Use this query to retrieve runs for a target. Arguments are `limit` (default `50`), `offset` (default `0`), `status`, `approach`, `search`, `sortBy`, `sortDirection`, `subjectId`, and `outcome`. The response is `PaginatedRuns { runs, hasMore, totalCount }`.

```graphql
query GetRuns($targetId: ID) {
  runs(targetId: $targetId) {
    hasMore
    totalCount
    runs {
      id
      name
      status
      exceptionCount
      startTime
      endTime
      target {
        id
        name
      }
    }
  }
}
```

`batch(id) { runs }` returns at most the 200 most recent runs - use the top-level `runs` query when you need more.

#### Get run details

```graphql
query GetRun($id: ID!) {
  run(id: $id) {
    id
    name
    status
    exceptionCount
    startTime
    endTime
    target {
      id
      name
    }
  }
}
```

Run statuses are lowercase: `pending`, `running`, `completed`, or `not_completed`. A run that concluded is `completed` whatever it found. For test-case runs the verdict is on `outcome`: `passed`, `failed` or `inconclusive` (`null` for explorations).

### Mutations

#### Create or update a target

Use `upsertSimulationTarget` to create a target the first time, then update it on later runs.

`buildKey` is the stable identity key for the target. Duku deduplicates by `subjectId` + `buildKey`. If you call the mutation again with the same pair, Duku updates the existing target instead of creating a duplicate.

For per-PR targets, use a stable key such as `github:repo=owner/repo:pr=123`.

Required input fields:

* `subjectId`: your Product ID
* `buildKey`: the stable identity key
* `name`: the target name shown in Viewport

Optional input fields:

* `description`
* `version`
* `environment`
* `buildNumber`
* `metadata`

`commitSha`, `parentCommitSha`, `lineage`, `label` and `environmentId` record the build as a release. They are available when Releases are enabled for your organisation, and refused with `FORBIDDEN` otherwise. Once you send `commitSha`, `buildKey` must identify that one commit (for example `github:repo=owner/repo:pr=123:sha=<sha>`).

A target inherits its URL from its product, so there is no target URL to set here. To explore a specific URL, pass `url` on `startExploration` (see below).

* `buildUrl` is deprecated and ignored. A target inherits its URL from its product; pass a per-run `url` on `startExploration` instead. Use `metadata` for build context - `metadata.ciRunUrl` renders as a **View CI run** link on the target.

```graphql
mutation Upsert($input: UpsertSimulationTargetInput!) {
  upsertSimulationTarget(input: $input) {
    id
    name
    version
  }
}
```

Example variables:

```json
{
  "input": {
    "subjectId": "sms_product_id",
    "buildKey": "github:repo=owner/repo:pr=123",
    "name": "PR #123"
  }
}
```

#### Start an exploration

```graphql
mutation StartExploration($input: StartExplorationInput!) {
  startExploration(input: $input) {
    id
    status
    batch {
      id
      runs {
        id
        name
      }
    }
  }
}
```

Variables:

```json
{
  "input": {
    "targetId": "smt_..."
  }
}
```

Supported input fields:

* `targetId`: target to explore
* `url`: URL to explore. For an API-key caller this per-run URL is treated as an override, and is only honoured when you also send `triggerSource: "CI"` (see below). Without it the `url` is ignored and the run uses the product's base URL (a target inherits its URL from its product).
* `githubRepository`: repository in `owner/repo` format
* `githubPrNumber`: pull request number as an integer
* `serverManagedComment`: boolean
* `triggerSource`: optional enum declaring where the run was triggered. Send `CI` from any CI provider - GitHub Actions, CircleCI, GitLab CI, and so on (the deprecated alias `GITHUB_ACTION` is treated identically). **Required whenever you pass `url`** from an API-key caller: it authorises the per-run URL override. It also labels the run as CI-triggered in your dashboards. Every other origin is derived from your credentials and cannot be declared here.
* `credentialSetId`: optional string that selects a saved credential set by ID or name. Duku resolves and decrypts it server-side, so you never send passwords in the request body. Credential fields from the set take precedence over inline values.
* `inputValues`: custom values for form filling, keyed by input type or name. Use `credentialSetId` for anything secret; values sent here travel in the request body.
* `waitForTargetSeconds` - optional. Wait for the URL to start serving before the exploration starts. Longer waits are shortened to the maximum, with a note in `warnings`.
* `environmentId` - explore whatever an environment currently serves (when Releases are enabled for your organisation). Use it instead of `targetId` and `url`.

The response includes `warnings` (a list of strings). When you send `serverManagedComment: true` together with `githubRepository` and `githubPrNumber`, Duku posts the PR comment itself and updates the same comment when the run finishes.

{% hint style="info" %}
**PR comments are posted by the Duku GitHub App, not GitHub Actions**

The App is a one-time install on the repository that lets Duku's platform post comments through the GitHub API. It is not GitHub Actions and uses no CI minutes.

Each product posts one sticky comment per PR. Later builds on the same PR update that comment in place.
{% endhint %}

#### Run all test cases

Use `runAllTestCases` to run the test cases configured on a target.

```graphql
mutation RunAllTestCases($input: RunAllTestCasesInput!) {
  runAllTestCases(input: $input) {
    success
    intentBatchId
  }
}
```

The input requires `targetId`. It optionally accepts `credentialSetId` by ID or name. It works the same way as on `startExploration`.

#### Polling for completion

After starting a run, poll the batch until its status is `COMPLETED` or `FAILED`.

Batch status is upper case - `PENDING` and `RUNNING` are in flight, `COMPLETED` and `FAILED` are terminal. Treat any other value as still in flight. Run status uses different values (see above). Poll no more often than every 30 seconds; explorations typically take 5 to 40 minutes. GitHub users should use \[/spaces/2rbi6bEnpVLUQHudgXDV/pages/HtRiEOTXrg4nOBAdS4YW "mention") for merge gating.

```graphql
query BatchStatus($id: ID!) {
  batch(id: $id) {
    id
    status
  }
}
```

### Trigger from any CI

Use this flow from CircleCI, GitLab CI, Jenkins, or any other pipeline. No GitHub Action is required.

1. Exchange the API key for an access token.

```bash
DECODED_KEY=$(printf '%s' "$PLATFORM_API_KEY" | base64 --decode)
CLIENT_ID=${DECODED_KEY%%:*}
CLIENT_SECRET=${DECODED_KEY#*:}

TOKEN=$(curl -s -X POST https://auth.duku.ai/realms/duku/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" | jq -r '.access_token')
```

2. Create or update the target, then capture the target ID.

```bash
TARGET_ID=$(curl -s -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation Upsert($input: UpsertSimulationTargetInput!) { upsertSimulationTarget(input: $input) { id } }",
    "variables": {
      "input": {
        "subjectId": "sms_product_id",
        "buildKey": "github:repo=owner/repo:pr=123",
        "name": "PR #123"
      }
    }
  }' | jq -r '.data.upsertSimulationTarget.id')
```

3. Start the exploration with the target ID, URL, PR metadata, and CI trigger source. `triggerSource: "CI"` is required here because a `url` is passed.

```bash
curl -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"query\": \"mutation StartExploration(\$input: StartExplorationInput!) { startExploration(input: \$input) { id status } }\",
    \"variables\": {
      \"input\": {
        \"targetId\": \"$TARGET_ID\",
        \"url\": \"https://preview.example.com\",
        \"githubRepository\": \"owner/repo\",
        \"githubPrNumber\": 123,
        \"serverManagedComment\": true,
        \"triggerSource\": \"CI\"
      }
    }
  }"
```

### End-to-end cURL example

```bash
DECODED_KEY=$(printf '%s' "$PLATFORM_API_KEY" | base64 --decode)
CLIENT_ID=${DECODED_KEY%%:*}
CLIENT_SECRET=${DECODED_KEY#*:}

TOKEN=$(curl -s -X POST https://auth.duku.ai/realms/duku/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" | jq -r '.access_token')

curl -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation StartExploration($input: StartExplorationInput!) { startExploration(input: $input) { id status } }",
    "variables": {
      "input": {
        "targetId": "smt_your_target_id"
      }
    }
  }'
```

### Schema introspection

Schema introspection is disabled on the production endpoint - `__schema` and `__type` queries fail validation. Write queries against the operations documented here; if you need a schema for code generation, contact your Duku account team.

### Common errors

| Code                    | Meaning                                                                                                                                                                                      |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UNAUTHENTICATED`       | Missing or invalid access token                                                                                                                                                              |
| `FORBIDDEN`             | Insufficient permissions for this operation. Refusals for a feature your organisation does not have carry `extensions.reason`, for example `VERSION_AXIS_NOT_ENABLED`                        |
| `NOT_FOUND`             | The resource does not exist, or belongs to another organisation                                                                                                                              |
| `BAD_USER_INPUT`        | Invalid input - including a missing URL when no override was sent and the product has no base URL, such as an environment with nothing deployed or no URL                                    |
| `CONFLICT`              | The request conflicts with the current state, such as a duplicate or a concurrent change, such as re-sending a `buildKey` for a different commit, or reaching the 10 API keys per user limit |
| `ORG_NOT_LINKED`        | The organisation is not fully provisioned - contact support                                                                                                                                  |
| `INTERNAL_SERVER_ERROR` | An unexpected failure. The message is always `Something went wrong. Please try again.` and `extensions.correlationId` carries an id to quote to support                                      |

GraphQL errors are returned with HTTP 200, so check the response body for an `errors` array rather than relying on the status code. Unexpected errors are masked to `INTERNAL_SERVER_ERROR` with a fixed message; the codes above always carry a specific one.

Three common causes: sending the base64 API key as the Bearer token instead of exchanging it first returns `UNAUTHENTICATED`; naming a product that belongs to another organisation returns `NOT_FOUND`; and getting an enum's casing wrong returns no code at all - the queried field comes back `null` and the reason is in `errors[]`.
