Corteksa

Frontend Integration

CRM — Frontend Integration

For web (React/Next.js) and mobile developers, plus external apps. After this page you can list, create, and update records without asking a backend engineer.

The records API accepts two credentials — pick one:

Authorization: Bearer <jwt>      # an admin login (dashboard UI)
X-Api-Key: crtk_live_…           # a scoped API key (external app)

Base URL: https://<workspace>.corteksa.com/api/v1.

Required permission / scope

Every record route is tagged with a route name derived from the object slug: read.{slug}, create.{slug}, update.{slug}, delete.{slug}.

To…You need
List / read recordsread.<object> (JWT role) or the read.<object> scope (API key)
Create a recordcreate.<object>
Update a recordupdate.<object>
Delete a recorddelete.<object>

For a JWT, these route names come from the caller's A/G/M/D object level (view ≠ D ⇒ read.<slug>, etc.) — see Authorization. For an API key, they come from the key's scopes (e.g. read.contacts, create.deals, or a read.* wildcard). Past the gate, rows are still filtered by the acting admin's access level, so a key never sees more than its owner.

The flow

1. Auth        login → JWT      OR   mint an API key with scopes
2. List fields GET  /object/:objectSlug/fields      → field slugs + types
3. List records GET /object/data/:objectSlug         → filter, paginate
4. Read one    GET  /object/data/:objectSlug/:recordSlug
5. Create      POST /object/data/:objectSlug          → body keyed by field slug
6. Update      PUT  /object/data/:objectSlug/:recordSlug

Create a record — body is keyed by field slug

This is the #1 gotcha. The data object is keyed by each field's slug, not its display name:

await fetch(`${BASE}/object/data/contacts`, {
  method: 'POST',
  headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    data: { 'name-8fk2': 'Sara Ali', 'email-p0zx': 'sara@acme.com', 'status-a1b2': 'Lead' },
  }),
});

Get the slugs from GET /object/:objectSlug/fields. Using the label ("Name") instead of the slug throws 400 "Field 'X' is required" — see Troubleshooting.

Filter & paginate

The list endpoint takes page / limit plus a filters array (JSON) and sort params:

GET /object/data/deals?page=1&limit=25&sortBy=amount-xy12&sortDirection=DESC
    &filters=[{"fieldSlug":"status-a1b2","operator":"equals","value":"Won"}]

Sorting a number field treats an empty value as the lowest value: it lands at the top of ASC and the bottom of DESC. Every other field type keeps the Postgres default, where an empty value sorts as the highest.

limit defaults to 25 (capped). Each filter is { fieldSlug, operator, value } — operators include equals, includes, greater_than, between, is_empty, before/after, this_month, … Also supported: adminSlug (assignee) and unassigned=true. For complex queries, POST /object/data/:objectSlug/search takes a searchTerm in the body.

UI concerns

  • Loading — reads are synchronous; import/export and some bulk ops are async jobs (poll the returned jobSlug status).
  • Field metadata — list/detail responses include a fields[] block (types, options, virtual relation fields) so you can render inputs without a second call. A freshly created record echoes the same { fields, data } shape as a fetched one.
  • Optimistic create/update — safe, but reconcile on the returned record slug; the response is the source of truth (server computes serial numbers, formulas, etc.).
  • Realtime — subscribe to the DataEventsGateway for live record changes instead of polling. See Events.
  • Errors400 (bad body / field slug), 403 (missing scope/level), 404 (wrong slug or out of your visibility). See Troubleshooting and Errors.

Next

On this page