C3 AI Documentation Home

Make API Requests

This page builds a working data page against your typed backend. You add a small client, read rows of a Type into a component, filter and count them, and write and remove a row while watching the count change. When you finish, you have a page that reads, filters, writes, and removes rows of a Type, and you can read the HTTP status of a failed call to tell which layer broke. The reference sections after the steps cover the query language, the body shapes, the failure statuses, and querying at scale.

Every interaction with the backend is one call, POST /api/8/{Type}/{action}, where the Type and the action, such as fetch or upsert, are both in the path. The steps build up that call. The examples use WindTurbine and ScadaReading to show that the pattern works with any Type — substitute your own Type name and fields, and the calls are identical.

Before you begin

Have the application running locally against a live environment, from The Local Dev Loop, and know the name of a Type in your data model with a few seeded rows.

Step 1: Add the client

Add a small client module at src/lib/c3.ts, so no page repeats the request plumbing. The client resolves the API base from the c3AppUrlPrefix cookie, posts the body, and throws on a failed status:

TypeScript
// src/lib/c3.ts
function apiPrefix(): string {
  const m = document.cookie.match(/(?:^|;\s*)c3AppUrlPrefix=([^;]+)/)
  if (m) return `${location.origin}/${decodeURIComponent(m[1]).replace(/^\/+|\/+$/g, '')}/`
  return (document.baseURI || location.href).replace(/#.*$/, '').replace(/[^/]*$/, '')
}

async function post<T>(type: string, action: string, args: unknown): Promise<T> {
  const res = await fetch(`${apiPrefix()}api/8/${type}/${action}`, {
    method: 'POST',
    credentials: 'same-origin',                                  // sends the c3auth cookie; see Authenticate Requests
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify(args),
  })
  if (!res.ok) throw new Error(`${type}.${action} HTTP ${res.status}`)
  return res.json()
}

// read: body is ["Type", spec]        write: body is [obj]
export const c3Read = <T>(type: string, action: string, spec?: object): Promise<T> =>
  post<T>(type, action, spec === undefined ? [type] : [type, spec])
export const c3Write = <T>(type: string, action: string, obj: object): Promise<T> =>
  post<T>(type, action, [obj])

export interface FetchResult<T> { type: string; objs: T[]; count: number; hasMore: boolean }

End state: c3Read and c3Write are importable from src/lib/c3.ts.

Step 2: Read rows into a component

Read rows of your Type in a component that fetches on mount, holds the result in state, and renders it. Substitute your Type name and fields for WindTurbine:

TypeScript
import { useEffect, useState } from 'react'
import { c3Read, type FetchResult } from './lib/c3'

interface WindTurbine { id: string; name: string }

export default function TurbinesPage() {
  const [rows, setRows] = useState<WindTurbine[] | null>(null)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    c3Read<FetchResult<WindTurbine>>('WindTurbine', 'fetch', { limit: 50 })
      .then((r) => setRows(r.objs))
      .catch((e) => setError(String(e)))
  }, [])                                          // [] runs the fetch once, on mount

  if (error)  return <div className="error">{error}</div>
  if (!rows)  return <div className="loading">Loading from C3…</div>
  return <ul>{rows.map((t) => <li key={t.id}>{t.name}</li>)}</ul>
}

End state: the component renders a list of rows from your Type.

Step 3: Filter and sort the read

Narrow and order the result by passing a FetchSpec. The filter is an Object Query Language (OQL) predicate, and it traverses a relationship by filtering a child Type on the parent reference id:

TypeScript
c3Read<FetchResult<ScadaReading>>('ScadaReading', 'fetch', {
  filter: "turbine.id == 'WT1'",
  order:  'descending(timestamp)',
  limit:  50,
})

End state: the component renders only the matching rows, in the order you set.

Step 4: Count without loading rows

Count the matching rows with fetchCount, which returns a bare number and never loads the rows, so it is far cheaper than fetching and measuring .length:

TypeScript
const n = await c3Read<number>('ScadaReading', 'fetchCount', { filter: "turbine.id == 'WT1'" })

End state: you have a count for the filter without loading the rows.

Step 5: Write and remove a row

Create or update a row with upsert, and delete it with remove. Both are writes, so the body is the bare object. upsert returns the written object with its new version, and remove returns true:

TypeScript
await c3Write('ScadaReading', 'upsert', { id: 'SR-TEST', turbine: { id: 'WT1' } })  // set a reference by its id
await c3Write('ScadaReading', 'remove', { id: 'SR-TEST' })

End state: a write and a delete both succeed, and a fetchCount before and after shows the count rise by one and return to its baseline. In a component, call fetch again after each write to refresh the rendered list — the user sees a new row appear after an upsert and a row disappear after a remove.

What you have now

You have a page that reads, filters, counts, writes, and removes rows of a Type through one client. The rest of this page is reference for the query language, the body shapes, the failure statuses, and scale.

Reference: the FetchSpec fields

The spec you pass to fetch or fetchCount carries the whole query in four fields. filter is an OQL predicate, combining comparisons with && and || and traversing relationships by reference id. include is a projection, where dotted paths pull fields from referenced Types, as in id, name, status, farm.id, farm.name. order sorts, wrapping a field in ascending(...) or descending(...). limit and offset page the result, and hasMore on the FetchResult tells you whether more rows remain.

Reference: the read-versus-write body shape

A read leads the array with the Type name, so the body is ["WindTurbine", spec]. A write sends the bare object with no Type name, so the body is [obj]. The c3Read and c3Write helpers build the correct shape for each, which is why every call routes through them rather than constructing the array inline.

The backend accepts three body encodings, and all three return 200: the positional array the helpers use, a named envelope ({thisArg, args} sent with an x-request-envelope: true header), and a named-kwargs {spec} object. Use the positional encoding, because it matches the base applications.

Reference: read the HTTP status when a call fails

The client throws with the HTTP status in the message, so read the status first, because it names the layer that broke.

StatusCauseWhat to check
401No credential reached the serverIn production, a missing or expired c3auth cookie; in development, the proxy failing to inject a token
302, or a redirect to /consoleThe base URL resolved wrongThe cluster, environment, and application are hardcoded, or the cookie is missing
404The Type or action does not existA misspelled Type name or a method the Type does not declare
500The server method threwThe call reached the backend; read the response body for the C3 error message

Telling an application-state problem from a data-model problem is the core debugging skill. A page that renders wrong rather than erroring is usually a failed call, a data shape that differs from the fields the component renders, or stale React state, so confirm the call succeeded and inspect the returned object before you change component code.

Reference: query efficiently as data grows

Filter and aggregate on the server, not in the browser, because a Type can hold far more rows than a browser should load. Filter server-side so only the rows you need cross the wire, count with fetchCount rather than fetching rows to measure them, and aggregate with evaluate and a group-by so the backend returns a summary rather than thousands of raw rows. The exact evaluate specification lives in the C3 Agentic AI Platform data-access documentation.

Reference: the clients you see in existing applications

Existing applications wrap this same call under several names, and they are all thin wrappers over POST /api/8/{Type}/{action}. c3Action and c3MemberAction are the positional per-application client the base applications hand-keep, c3Call is the genesis client that uses the named-envelope body, useC3Action is a C3 Agentic AI Platform-provided React hook, and c3Fetch is one application's read wrapper. None is a shared package you import; each application copies or writes its own, so wrap the call in one client of your own and stay consistent.

See also

Was this page helpful?