.md

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

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)
  • Scripted endpoints (part of a paid plan) — your own routes answered by project scripts, see 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
  • 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)

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:
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:

curl 'https://runner.example.com/instance/your-project-id-customer-api/status?api-key=your-api-key'
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.

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

API endpoints

Status endpoint

Check if the instance is ready:

status-request.sh
GET /instance/your-project-id-customer-api/status?api-key=your-key
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 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:

overview-request.sh
GET /instance/your-project-id-customer-api/overview?api-key=your-key
overview-response.json
{
  "models": [
    { "key": "customer", "count": 1200 },
    { "key": "order", "count": 48 }
  ]
}

Details and invocations

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 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, mcp:<tool> (mcp:query, mcp:endpoint:<path>) for MCP 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 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()
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 and the 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. queries combine with 'and' unless you set groupType: 'or'.

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" }] }'
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:

endpoints-config.json
{
  "endpoints": [{ "path": "orders/:id", "method": "GET", "scriptKey": "order-detail" }]
}
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 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 for turning it on, connecting an agent, the tools and the error answers.

v-Guard caching

Enable version-based cache invalidation for improved performance:

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 — 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.

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

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

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

Endpoints not responding

Call the status endpoint first: its status says whether the instance is loaded, and a 403 means the API key or the instance key 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