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 records | read.<object> (JWT role) or the read.<object> scope (API key) |
| Create a record | create.<object> |
| Update a record | update.<object> |
| Delete a record | delete.<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/:recordSlugCreate 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
jobSlugstatus). - 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
DataEventsGatewayfor live record changes instead of polling. See Events. - Errors —
400(bad body / field slug),403(missing scope/level),404(wrong slug or out of your visibility). See Troubleshooting and Errors.
Next
- Copy-paste code → Examples
- Every endpoint → REST API
- One-page recipe → Create a record