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

# GraphQL API

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](/integrations/triggering-explorations-from-any-ci.md). 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](/integrations/api-keys.md).

### 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 `smt_...` and `key_...` are placeholders.
{% endhint %}

### Queries

#### List products

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

#### List targets

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

#### List runs for a target

Use this query to retrieve recent runs for a target.

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

#### Get run details

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

Run statuses are lowercase: `pending`, `running`, `completed`, or `failed`.

### 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`
* `buildUrl`
* `metadata`

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

Example variables:

```json
{
  "input": {
    "subjectId": "smt_product_id",
    "buildKey": "github:repo=owner/repo:pr=123",
    "name": "PR #123",
    "buildUrl": "https://preview.example.com"
  }
}
```

#### 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
* `githubRepository`: repository in `owner/repo` format
* `githubPrNumber`: pull request number as an integer
* `serverManagedComment`: boolean
* `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.

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 target posts its own sticky comment on a PR. Duku updates that comment in place when the run finishes.
{% 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`.

```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": "smt_product_id",
        "buildKey": "github:repo=owner/repo:pr=123",
        "name": "PR #123",
        "buildUrl": "https://preview.example.com"
      }
    }
  }' | jq -r '.data.upsertSimulationTarget.id')
```

3. Start the exploration with the target ID, URL, and PR metadata.

```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
      }
    }
  }"
```

### 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"
      }
    }
  }'
```

### Common errors

| Code              | Meaning                         |
| ----------------- | ------------------------------- |
| `UNAUTHENTICATED` | Missing or invalid access token |
| `FORBIDDEN`       | Insufficient permissions        |
| `NOT_FOUND`       | Resource does not exist         |
| `BAD_USER_INPUT`  | Invalid input                   |
