# Kopra API Integration Guide Kopra is a multi-tenant custom field management API. It lets your SaaS customers configure their own custom fields and store values against entities in your system. ## Authentication Send `X-API-Key: {YOUR_API_KEY}` header with every request. Base URL: `https://api.kopra.dev/api` ## Core Concepts - **Field Group**: A category of fields (e.g., "Customer Profile", "Invoice Details"). Create first. - **Global Field**: A reusable field definition shared across all tenants within a field group. - **Tenant Field**: A custom field defined by a specific tenant within a field group. - **Field Value**: The actual data stored for a field on a specific entity. - **Tenant**: Your customer. Use their unique ID (database ID, slug, etc.) as the tenantId. Tenants are created automatically on first use - no setup needed. ## Quick Start ### 1. Create a Field Group ``` POST /api/field-groups Content-Type: application/json X-API-Key: your-api-key { "name": "Customer Profile", "description": "Custom fields for customer records", "key": "customer" } ``` ### 2. Create Global Fields (shared across all tenants) ``` POST /api/global-fields Content-Type: application/json X-API-Key: your-api-key { "key": "company_size", "label": "Company Size", "schema": { "type": "select", "validation": { "required": true, "options": ["1-10", "11-50", "51-200", "200+"] } }, "fieldGroupIds": ["{field_group_id}"] } ``` ### 3. Save Field Values for an Entity ``` POST /api/tenants/{tenant_id}/field-groups/{field_group_id}/entities/{entity_id}/values Content-Type: application/json X-API-Key: your-api-key { "company_size": "51-200", "industry": "Healthcare" } ``` ### 4. Read Field Values ``` GET /api/tenants/{tenant_id}/field-groups/{field_group_id}/entities/{entity_id}/values X-API-Key: your-api-key ``` ### 5. Generate an Embed Token Tokens are scoped to (clientId, tenantId). One token works for any field group or entity within the tenant. ``` POST /api/auth/token Content-Type: application/json X-API-Key: your-api-key { "tenantId": "tenant-123", "fieldGroupKey": "customer" } ``` Response: ```json { "success": true, "data": { "token": "eyJ...", "fieldEditorUrl": "https://your-kopra.com/embed/field-editor", "tenantConfigUrl": "https://your-kopra.com/embed/tenant-config", "fieldsGroupId": "uuid-of-resolved-field-group", "expiresAt": "2026-03-28T12:00:00.000Z" } } ``` ### 6. Embed with the TypeScript SDK ``` npm install @kopra-dev/sdk ``` **Your backend** (token endpoint - keeps API key server-side): ```typescript // Express example app.post('/api/kopra-token', async (req, res) => { const response = await fetch('https://api.kopra.dev/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.KOPRA_API_KEY, }, body: JSON.stringify(req.body), // { tenantId, fieldGroupKey } }); const { data } = await response.json(); res.json(data); }); ``` **Your frontend** (SDK initialization): ```typescript import { KopraSDK } from '@kopra-dev/sdk'; const sdk = new KopraSDK({ currentTenant: 'your-customer-id', getToken: async (req) => { const res = await fetch('/api/kopra-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req), }); return res.json(); }, onFieldsSaved: (values) => console.log('Saved:', values), onFieldsError: (error) => console.error('Error:', error), }); // Load field editor - values autosave after 2s of inactivity await sdk.loadCustomFields('container-id', { fieldGroupKey: 'customer', entityId: 'contact-123', }); // Load tenant config panel (your customer manages their own fields) await sdk.loadFieldConfiguration('config-container', { fieldGroupKey: 'customer', fieldLimit: 10, }); // Manual save (autosave is on by default, but you can also trigger manually) const result = await sdk.saveFields(); // { success: true, values: { ... } } ``` **SDK save behavior:** - Autosave is enabled by default (2s debounce after last change) - The embedded editor shows "Saving..." / "Changes saved" status - `onFieldsSaved` callback fires on each save - To disable: `new KopraSDK({ autosave: { enabled: false }, ... })` - To save manually: `await sdk.saveFields()` ## MCP Server (AI Agent Integration) Kopra has an MCP server (`@kopra-dev/mcp`) that lets AI agents manage custom fields via the Model Context Protocol. **Claude Code:** ```bash claude mcp add kopra -- npx -y @kopra-dev/mcp ``` **Claude Desktop / Cursor** (add to config JSON): ```json { "mcpServers": { "kopra": { "command": "npx", "args": ["-y", "@kopra-dev/mcp"], "env": { "KOPRA_API_KEY": "your-api-key" } } } } ``` API key is optional at install time. Without it, each tool call accepts `apiKey` as a parameter. Available MCP tools (16 total): - Field Groups: list, get, create, update, delete - Global Fields: list, create, delete - Tenant Fields: list, create, delete - Field Values: get, save, search - Webhooks: list, create ## Theming (White-Label) The embedded field editor is fully white-label. It runs inside an iframe but you control every visual element through a theme object. Your users see YOUR design, not Kopra's branding. ```typescript await sdk.loadCustomFields('container', { fieldGroupKey: 'customer', entityId: 'contact-123', theme: { container: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', fontFamily: 'inherit' }, label: { fontSize: '14px', fontWeight: '500', color: '#374151' }, input: { padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px' }, inputFocus: { borderColor: '#3b82f6', boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)' }, select: { padding: '8px 12px', border: '1px solid #d1d5db', borderRadius: '6px' }, textarea: { padding: '8px 12px', borderRadius: '6px', minHeight: '80px' }, fieldGlobal: { borderLeft: '3px solid #3b82f6', paddingLeft: '10px' }, fieldTenant: { borderLeft: '3px solid #10b981', paddingLeft: '10px' }, }, }); ``` Theme keys: `container`, `fieldContainer`, `fieldContainerSingle`, `fieldGlobal`, `fieldTenant`, `label`, `input`, `inputFocus`, `select`, `textarea`, `saveButton`. Each key also has a `Class` variant (e.g., `inputClass`) that accepts a CSS class name instead of inline styles. The SDK ships with a default theme. Retrieve it with `sdk.getDefaultTheme()`. ## Per-Tenant Tiering You can offer different field limits to your tenants based on their plan. Pass `fieldLimit` when generating a token: ```typescript // Your backend - set field limit based on YOUR customer's plan app.post('/api/kopra-token', async (req, res) => { const tenant = await db.getTenant(req.body.tenantId); const fieldLimit = tenant.plan === 'free' ? 5 : tenant.plan === 'pro' ? 20 : 100; const response = await fetch('https://api.kopra.dev/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.KOPRA_API_KEY, }, body: JSON.stringify({ tenantId: req.body.tenantId, fieldGroupKey: req.body.fieldGroupKey, fieldLimit, // Enforced server-side via the JWT token }), }); const { data } = await response.json(); res.json(data); }); ``` The `fieldLimit` is baked into the JWT token. When a tenant tries to create more fields than their limit, the embed UI shows an error. This lets you tier Kopra features by your own pricing plans without any Kopra plan changes. ## All REST Endpoints ### Field Groups - `GET /api/field-groups` - List all - `POST /api/field-groups` - Create (body: name, description, key) - `GET /api/field-groups/:id` - Get one - `PUT /api/field-groups/:id` - Update (all fields optional) - `DELETE /api/field-groups/:id` - Soft delete ### Global Fields - `GET /api/global-fields` - List all - `POST /api/global-fields` - Create (body: key, label, schema, fieldGroupIds) - `GET /api/global-fields/:id` - Get one - `PUT /api/global-fields/:id` - Update (all fields optional) - `DELETE /api/global-fields/:id` - Soft delete ### Tenant Fields - `GET /api/tenants/:tenantId/field-groups/:fgId/fields` - List - `POST /api/tenants/:tenantId/field-groups/:fgId/fields` - Create - `PUT /api/tenants/:tenantId/field-groups/:fgId/fields/:id` - Update - `DELETE /api/tenants/:tenantId/field-groups/:fgId/fields/:id` - Soft delete ### Field Values - `GET /api/tenants/:tenantId/field-groups/:fgId/entities/:entityId/values` - Get values - `POST /api/tenants/:tenantId/field-groups/:fgId/entities/:entityId/values` - Save values - `DELETE /api/tenants/:tenantId/field-groups/:fgId/entities/:entityId/values` - Delete values - `GET /api/field-values/search?fieldGroupId=&tenantId=&fieldKey=&value=&page=&pageSize=` - Search ### Webhooks - `GET /api/webhooks` - List endpoints - `POST /api/webhooks` - Create (body: url, events[]) - `PUT /api/webhooks/:id` - Update - `DELETE /api/webhooks/:id` - Delete - `GET /api/webhooks/:id/deliveries` - View delivery history ### Tokens - `POST /api/auth/token` - Generate embed token (body: tenantId, fieldGroupKey?, fieldLimit?) ## Field Types string, number, boolean, date, select, textarea, email, url, multiselect, enum, json, text ## Field Schema Format ```json { "type": "select", "validation": { "required": true, "minLength": 1, "maxLength": 500, "min": 0, "max": 100, "options": ["Option A", "Option B"] }, "uiHints": { "placeholder": "Select an option...", "helpText": "Choose the best match" } } ``` ## Webhook Events field_group.created, field_group.updated, field_group.deleted, global_field.created, global_field.updated, global_field.deleted, tenant_field.created, tenant_field.updated, tenant_field.deleted, field_value.saved, field_value.deleted Webhook payloads include `X-Kopra-Signature` header (HMAC-SHA256 of the payload using your webhook secret). ## Error Responses All errors return JSON: `{ "error": "message" }` | Status | Meaning | Example | |--------|---------|---------| | 400 | Validation error | Empty name, unknown field key, invalid select option, value too long | | 401 | Authentication failed | Missing or invalid API key / JWT token | | 403 | Plan limit reached | Field group limit, tenant limit, field value limit | | 404 | Not found | Invalid field group ID, deleted resource | | 409 | Conflict | Duplicate field group key, duplicate field key | | 429 | Rate limited | Too many API calls or token requests | Validation errors include field-level details: ```json { "error": "Validation failed", "details": [{ "field": "key", "message": "String must contain at least 1 character(s)" }] } ``` ## Rate Limits - Every response includes `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers. - Authentication endpoints have stricter limits than general API calls. - If you receive a 429 response, use the `RateLimit-Reset` header to determine when to retry. ## Resources **API & SDK:** - OpenAPI 3.0 JSON: `/api/docs.json` - Interactive API docs: `/api/docs` - LLM context (this document): `/api/llm-context` - TypeScript SDK: `npm install @kopra-dev/sdk` - MCP Server: `claude mcp add kopra -- npx -y @kopra-dev/mcp` **Guides & tutorials:** - [Building Workflows on Custom Field Data with Webhooks and REST API](https://kopra.dev/blog/workflows-with-kopra-webhooks-api) - [Adding Custom Fields to an Express + React App: A Complete Walkthrough](https://kopra.dev/blog/integration-walkthrough-express-react) - [Custom Fields for Booking and Healthcare Applications](https://kopra.dev/blog/custom-fields-for-booking-healthcare) - [How Kopra Secures Your Customer Data](https://kopra.dev/blog/kopra-security-architecture) - [Add Custom Fields to Your React App in 5 Minutes](https://kopra.dev/blog/add-custom-fields-react-app) - [Adding Custom Fields to Your Project Management Tool](https://kopra.dev/blog/custom-fields-for-project-management) - [Custom Fields for CRM Applications: What You Need to Know](https://kopra.dev/blog/custom-fields-for-crm) - [Build vs Buy: Custom Fields for Your SaaS Product](https://kopra.dev/blog/build-vs-buy-custom-fields) - [Custom Fields in Multi-Tenant SaaS: The Complete Architecture Guide](https://kopra.dev/blog/multi-tenant-custom-fields-architecture) - [How to Build a Custom Fields System for Your SaaS](https://kopra.dev/blog/how-to-build-custom-fields) - [Why Every B2B SaaS Eventually Needs Custom Fields](https://kopra.dev/blog/why-every-saas-needs-custom-fields)