# REST APIs

A REST API (RAPI) serves your dcupl application over HTTP: an instance on an app runner loads the application once and answers queries against it, with an OpenAPI document and an API key.

## Overview

```mermaid
flowchart LR
  subgraph Console
    T[API Template]
    D[Deploy]
  end

  subgraph Runner
    I[REST API instance]
    E[Endpoints]
  end

  subgraph Client["Client Apps"]
    W[Web App]
    M[Mobile App]
    B[Backend]
  end

  T --> D
  D --> I
  I --> E
  E --> W
  E --> M
  E --> B
```

A REST API instance provides:

- **Query endpoints** for every model of the loaded application — `query`, `facet`, `aggregate`, `pivot`, `suggest`, `metadata` (see [Query endpoints](#query-endpoints))
- **Scripted endpoints** (part of a paid plan) — your own routes answered by project scripts, see [Scripted endpoints](/console/rapi/scripted-endpoints)
- **MCP endpoint** (part of a paid plan) — let AI agents such as Claude Code query an instance with its API key, see [MCP endpoint](/console/rapi/mcp)
- **OpenAPI/Swagger documentation** for API exploration
- **API key authentication** — one key per template, sent as header or query parameter
- **v-Guard caching** for version-based cache invalidation
- **A status route** — what the instance loaded, and why a start failed (see [Status Endpoint](#status-endpoint))

## Setting up a REST API

The **REST APIs** entry appears for project owners and editors. REST APIs are on for new projects; if the entry is missing, ask your dcupl contact to turn them on for your project. The number of deployed instances per project is capped; the Templates tab shows it as _Deployed: X/max_.

### Step 1: create an API template

1. Navigate to **REST APIs** in your project
2. On the **Templates** tab, click **+** (or **Template** while the project has none) and enter a name — the Console creates `api/<key>.api.json` and opens it in the form editor
3. Fill in the configuration:

```json filename="api/customer-api.api.json"
{
  "key": "customer-api",
  "name": "Customer API",
  "description": "REST API for customer data",
  "vGuardEnabled": false,
  "auth": [
    {
      "type": "api-key",
      "value": "your-secure-api-key-here"
    }
  ],
  "dcuplConfig": {
    "projectId": "your-project-id",
    "apiKey": "your-sdk-readonly-key",
    "processOptions": {
      "applicationKey": "production",
      "environmentKeys": ["prod"]
    }
  }
}
```

### Step 2: deploy to runner

1. Open the template card's **⋮** menu and choose **Deploy** (a template that is not deployed yet also shows a **Not deployed** tag that opens the same form), or click the deploy button in the toolbar
2. In _Deploy an Instance_, pick the **Runner** and the **Template**
3. Click **Deploy**
4. Open the runner's tab and wait for the status to show `ready`

An instance loads its data once, when it starts. To pick up changed models, data or template settings, select it on the runner's tab and click **Update** (a redeploy); the instance answers from the previous data until the reload is done (status `updating`).

### Step 3: access your API

Your API is now available at:

```
{runner-url}/instance/{instance-key}/
```

`{runner-url}` is the URL shown on the runner's tab. `{instance-key}` is the key in the **Instance** column of that tab — deploying prefixes the template key with your project ID (`<projectId>-customer-api`), so it is neither the template key alone nor the API key. A runner set up without the prefix uses the bare template key; the **Instance** column always shows the key to call. The examples below write it as `your-project-id-customer-api`.

## Authentication

### API key authentication

An instance supports API key authentication via query parameter or header:

#### Query Parameter

```bash
curl 'https://runner.example.com/instance/your-project-id-customer-api/status?api-key=your-api-key'
```

#### Header

```bash
curl 'https://runner.example.com/instance/your-project-id-customer-api/status' \
  -H 'api-key: your-api-key'
```

---

The query parameter may also be spelled `apiKey` or `Api-Key`; as a header only `api-key` works (header names are case-insensitive, `apiKey` is not accepted). A missing or wrong key — and an unknown instance key — is answered with `403 Forbidden resource`.

**Best Practice:** Use header-based authentication in production as it's more secure and doesn't expose keys in URLs or logs.

### One API key per instance

The runner checks the **first** `api-key` entry of `auth` and ignores any further entries, so give each consumer the same key (the Console form has a single **Api Key** field). A template without an `api-key` entry, or with an empty value, deploys an **open** instance that answers anyone who knows its URL.

```json filename="auth-config.json"
{
  "auth": [{ "type": "api-key", "value": "your-secure-api-key-here" }]
}
```

## API endpoints

### Status endpoint

Check if the instance is ready:

```bash filename="status-request.sh"
GET /instance/your-project-id-customer-api/status?api-key=your-key
```

```json filename="status-response.json"
{
  "key": "your-project-id-customer-api",
  "uid": "…",
  "status": "ready",
  "v": "3f2a…",
  "createdAt": "2026-10-12T09:00:00.000Z",
  "changedAt": "2026-10-12T09:00:00.000Z",
  "instanceChangedAt": "2026-10-12T09:00:04.000Z",
  "report": {
    "resources": { "scheduled": 4, "skipped": [], "unresolvedVariables": [] },
    "quality": { "errorCount": 0, "errors": [] },
    "models": 3,
    "items": 1250,
    "durationMs": 3120
  }
}
```

`v` is the version hash that [v-Guard](#v-guard-caching) checks. `report` describes what the last start loaded (resources skipped with the reason, the first 20 quality errors). While the status is `error`, an `error` object (`message`, `name`, `phase`: `fetch-config`, `process`, `init` or `worker`, `at`) says why. `templateVersion` and `runnerInstanceId` are optional.

**Status values:**

| Status       | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `waiting`    | Registered, not started yet                                                 |
| `processing` | Loading the loader configuration and data                                   |
| `ready`      | Accepting requests                                                          |
| `updating`   | Reloading after a configuration change; the previous data answers meanwhile |
| `error`      | Start failed (or the instance crashed later) — see `error`                  |
| `stopped`    | Stopped on the runner                                                       |

The data routes answer `400 instance not ready` while the status is `waiting`, `processing`, `error` or `stopped`.

### Overview endpoint

List the models of the loaded application with their item counts:

```bash filename="overview-request.sh"
GET /instance/your-project-id-customer-api/overview?api-key=your-key
```

```json filename="overview-response.json"
{
  "models": [
    { "key": "customer", "count": 1200 },
    { "key": "order", "count": 48 }
  ]
}
```

### Details and invocations

```bash filename="details-request.sh"
GET /instance/your-project-id-customer-api/details?api-key=your-key
GET /instance/your-project-id-customer-api/invocations?action=query&api-key=your-key
```

`details` is the [status response](#status-endpoint) plus `runtime` (the instance's heap figures, `used_heap_size` and `max_heap_size`, in megabytes; both `0` when the instance cannot answer, e.g. while it is not ready) and `invocations`. `invocations` lists the instance's call counters, each with `completed` and `failed`: one row for the instance and one per action — the query routes by name (`query`, `facet`, …), `endpoint` and `endpoint:<path>` for [scripted endpoints](/console/rapi/scripted-endpoints), `mcp:<tool>` (`mcp:query`, `mcp:endpoint:<path>`) for [MCP](/console/rapi/mcp#monitoring) calls. `action` selects one of them. The counters live in the runner's memory and start from zero when the runner restarts. Unlike `status`, `overview` and `details`, `invocations` needs the current `v` while [v-Guard](#v-guard-caching) is on.

### Swagger documentation

Open the interactive Swagger UI in a browser:

```
GET /instance/your-project-id-customer-api/swagger
```

The OpenAPI 3.0 document itself (JSON) is served at `GET /instance/your-project-id-customer-api/swagger-json`. Neither needs the API key — they describe the routes and contain no secrets; enter the key in the Swagger UI to try requests.

### Query endpoints

The data routes run the dcupl SDK's query and function API against the instance's loaded data. Each exists as `GET` (options as a JSON string in the `data` query parameter) and `POST` (the same options as a JSON body, answered with `201`):

| Route       | Options                                    | Runs                    |
| ----------- | ------------------------------------------ | ----------------------- |
| `query`     | `{ modelKey, queries, start?, count?, … }` | `dcupl.query.execute()` |
| `facet`     | `{ modelKey, options, query? }`            | `dcupl.fn.facets()`     |
| `aggregate` | `{ modelKey, options, query? }`            | `dcupl.fn.aggregate()`  |
| `pivot`     | `{ modelKey, options, query? }`            | `dcupl.fn.pivot()`      |
| `suggest`   | `{ modelKey, options, query? }`            | `dcupl.fn.suggest()`    |
| `metadata`  | `{ modelKey, query? }`                     | `dcupl.fn.metadata()`   |

```bash title="Query the first 10 active customers"
curl -X POST 'https://runner.example.com/instance/your-project-id-customer-api/query' \
  -H 'api-key: your-api-key' \
  -H 'content-type: application/json' \
  -d '{ "modelKey": "customer", "queries": [{ "attribute": "status", "operator": "eq", "value": "active" }], "count": 10 }'
```

The response is the SDK's result as JSON (for `query`, the array of items). The options are those of [Functions](/sdk/api/functions) and the [query operators](/sdk/api/query-operators). An unparsable `data` string, invalid options or an instance that is not `ready` yield `400`.

### Query examples

There are no routes per model: name the model in `modelKey` and describe what you want with dcupl [query operators](/sdk/api/query-operators). `queries` combine with `'and'` unless you set `groupType: 'or'`.

```bash title="Get one item by key"
curl -X POST 'https://runner.example.com/instance/your-project-id-customer-api/query' \
  -H 'api-key: your-api-key' -H 'content-type: application/json' \
  -d '{ "modelKey": "customer", "queries": [{ "attribute": "key", "operator": "eq", "value": "c1" }] }'
```

```bash title="Filter, sort and page"
curl -X POST 'https://runner.example.com/instance/your-project-id-customer-api/query' \
  -H 'api-key: your-api-key' -H 'content-type: application/json' \
  -d '{
    "modelKey": "customer",
    "queries": [
      { "attribute": "status", "operator": "in", "value": ["active", "trial"] },
      { "attribute": "age", "operator": "gt", "value": 25 }
    ],
    "sort": { "attributes": ["name"], "order": ["ASC"] },
    "start": 20,
    "count": 10
  }'
```

`sort.order` takes `'ASC'` or `'DESC'` (uppercase), one per attribute. `start` (0-based) and `count` page the result.

## Scripted endpoints

Besides the query routes, a template can declare its own routes under `/instance/{key}/endpoints/…`, each answered by a script from your project:

```json filename="endpoints-config.json"
{
  "endpoints": [{ "path": "orders/:id", "method": "GET", "scriptKey": "order-detail" }]
}
```

```bash
GET /instance/your-project-id-customer-api/endpoints/orders/42?api-key=your-key
```

The script receives the request (path parameters, query, JSON body) and a read-only view of the instance's data; its return value is the JSON response. Scripted endpoints are part of a paid plan that dcupl turns on for your project; ask your dcupl contact. See [Scripted endpoints](/console/rapi/scripted-endpoints) for the template fields, the route, status codes and the Console tooling.

## MCP endpoint

With `"mcp": { "enabled": true }` in the template, the instance also serves `POST /instance/{key}/mcp`, a Model Context Protocol endpoint. Any MCP-capable agent — Claude Code, Cursor, VS Code — connects to it with the URL and the instance's API key and gets read-only tools over the deployed data plus one tool per scripted endpoint. MCP is part of a paid plan that dcupl turns on for your project; ask your dcupl contact. See [MCP endpoint](/console/rapi/mcp) for turning it on, connecting an agent, the tools and the error answers.

## v-Guard caching

Enable version-based cache invalidation for improved performance:

```json filename="vguard-config.json"
{
  "vGuardEnabled": true
}
```

### How v-Guard works

Data responses (the query endpoints and scripted endpoints) are cacheable for an hour: `cache-control: private, max-age=3600` for an instance with an API key, `public, max-age=3600` for an open one, `no-store` on errors. The instance's `v` — a hash of its deployed configuration, read from [`/status`](#status-endpoint) — changes with every redeploy (**Update**), not when only the data behind it changes. With `vGuardEnabled: true`, every data request must carry the current `v` as query parameter or header, so a new configuration means new URLs and old cache entries are never reused. A missing or outdated `v` is answered with `400 Invalid v parameter`.

```mermaid
sequenceDiagram
  participant Client
  participant RAPI as REST API instance

  Client->>RAPI: GET /status
  RAPI-->>Client: { status: "ready", v: "3f2a…" }
  Client->>RAPI: POST /query?v=3f2a…
  alt v matches
    RAPI-->>Client: 201 result (cacheable)
  else v outdated or missing
    RAPI-->>Client: 400 Invalid v parameter
    Client->>RAPI: GET /status (read the new v, retry)
  end
```

### Client integration

```typescript filename="vguard-client.ts"
class RAPIClient {
  private v: string | null = null;

  constructor(
    private baseUrl: string, // {runner-url}/instance/{instance-key}
    private apiKey: string,
  ) {}

  private async currentV(): Promise<string> {
    if (!this.v) {
      const response = await fetch(`${this.baseUrl}/status`, { headers: { 'api-key': this.apiKey } });
      this.v = (await response.json()).v as string;
    }
    return this.v;
  }

  async query(options: object, retry = true): Promise<unknown> {
    const v = await this.currentV();
    const response = await fetch(`${this.baseUrl}/query?v=${encodeURIComponent(v)}`, {
      method: 'POST',
      headers: { 'api-key': this.apiKey, 'content-type': 'application/json' },
      body: JSON.stringify(options),
    });

    // 400: the configuration changed (or the instance is not ready) — re-read v once
    if (response.status === 400 && retry) {
      this.v = null;
      return this.query(options, false);
    }

    return response.json();
  }
}
```

**When to enable v-Guard:** when responses pass through a browser or CDN cache and a client must never mix answers from two deployments. The cost: every client has to read `v` from `/status` first and read it again after a `400`.

**When to leave it off:** for simple server-to-server callers. The one-hour `cache-control` applies either way.

## Configuration reference

### Template structure

```typescript filename="template-types.ts"
interface RAPITemplate {
  key: string; // Unique identifier (alphanumeric, -, _)
  name?: string; // Display name
  description?: string; // Optional description
  version?: string; // Optional version string
  vGuardEnabled: boolean; // Enable version-based caching
  auth?: AuthConfig[]; // Only the first api-key entry is checked; none = open instance
  dcuplConfig: DcuplConfig; // dcupl integration config
  endpoints?: TemplateEndpoint[]; // Scripted endpoints (need them on your plan, see /console/rapi/scripted-endpoints)
  mcp?: { enabled: boolean }; // Serve POST /instance/{instance-key}/mcp (needs MCP on your plan, see /console/rapi/mcp)
}

interface AuthConfig {
  type: 'api-key';
  value: string;
}

interface TemplateEndpoint {
  path: string; // e.g. 'orders/:id'
  method: 'GET' | 'POST';
  scriptKey: string;
  description?: string;
}

interface DcuplConfig {
  projectId: string;
  apiKey: string; // SDK readonly key
  processOptions: {
    applicationKey?: string; // Select application preset
    environmentKeys?: string[]; // Select environments
    resourceTags?: (string | string[])[]; // Filter by resource tags
    variables?: { key: string; value: string | undefined; description?: string }[];
    options?: AppLoaderConfiguration.Options; // Replace the loader file's top-level options, see /sdk/api/loader#processoptions
  };
  loaderFetchOptions?: {
    baseUrl?: string; // Custom CDN URL
    loaderFileName?: string; // Custom loader filename
    headers?: { key: string; value: string }[]; // Sent when fetching the loader file from a custom baseUrl, e.g. authorization
  };
}
```

## Troubleshooting

### Instance status: error

**Causes:**

- Invalid SDK API key
- Project not accessible
- Data loading failed

**Solutions:**

1. Verify SDK API key in template
2. Check project permissions
3. Read `error.phase` / `error.message` and `report` in the [status response](#status-endpoint)

### Endpoints not responding

Call the [status endpoint](#status-endpoint) first: its `status` says whether the instance is loaded, and a `403` means the API key or the [instance key](#step-3-access-your-api) is wrong (the instance key carries your project ID as a prefix).

### High memory usage

**Causes:**

- Large datasets
- Too many models loaded

**Solutions:**

1. Load fewer resources: select them with `processOptions.resourceTags` or an application whose tags cover only what the API needs
2. Contact dcupl if the instance still needs more memory than its runner allows

## Next steps

- [Scripted endpoints](/console/rapi/scripted-endpoints) — Add your own routes to an instance
- [MCP endpoint](/console/rapi/mcp) — Let an AI agent query an instance
- [Workflows](/console/workflows) — Build data processing pipelines
- [Load from the console](/sdk/guides/load-from-console) — Load the same application in your app with the dcupl SDK
