## Summary - **Public domains can now be bound to a specific app.** When a request hits an app-bound public domain, route resolution restricts logic-function matching to that app's HTTP-routed functions only — isolating each app's routes to its own domain instead of letting routes from other apps in the workspace match nondeterministically. - **Settings sidebar reorganized.** Removed the standalone Domains page. Workspace Domain → General. Approved Domains + Invitations → Members "Access" tab. Emailing Domains + Public Domains → Apps "Developer" tab. Roles → Members "Roles" tab. ## Why The use case: someone building a partner portal app or a lead-collection app declares private objects (leads, partners…) plus a few public HTTP routes. Each app needs its own domain (`partners.acme.com`, `leads.acme.com`) without those domains exposing every other app's routes in the same workspace. Today's PublicDomainEntity is workspace-scoped only, so all HTTP-routed logic functions in a workspace compete for any public domain — first match wins nondeterministically. ## Backend - Added nullable `applicationId` FK to `PublicDomainEntity` (cascade-deleted with the app); indexed for the route-trigger lookup. - New fast instance command `2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain` adds the column, index, and FK constraint. - `createPublicDomain(domain, applicationId)` accepts an optional app binding; new `updatePublicDomain(domain, applicationId)` mutation rebinds/unbinds an existing domain. Both validate the application belongs to the workspace. - `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain(origin)` returns both the workspace and the matched public domain in one query — replacing the old back-to-back lookups in the route-trigger hot path. `getWorkspaceByOriginOrDefaultWorkspace` is preserved as a thin wrapper. - `RouteTriggerService` filters `logicFunction` by `applicationId` when the matched public domain is app-scoped; falls back to workspace-wide when unbound. - Three sequential validation queries in `createPublicDomain` now run in parallel via `Promise.all`. ## Frontend | Old location | New location | |---|---| | Settings sidebar → Domains (standalone page) | Removed | | Domains page → Workspace Domain | General page | | Domains page → Approved Domains | Members → Access tab | | Domains page → Emailing Domains | Apps → Developer tab | | Domains page → Public Domains | Apps → Developer tab | | Settings sidebar → Roles (standalone) | Members → Roles tab | | `pages/settings/roles/` | `pages/settings/members/roles/` | - The Public Domain detail page has an Application picker that uses `Select`'s native `emptyOption` + `null` value pattern (matches `SettingsDataModelObjectIdentifiersForm`). - Members page tabs use the existing `TabListFromUrlOptionalEffect` mechanism (rendered automatically by `TabList`) for hash-based tab activation. - `/settings/members/roles` redirects to `/settings/members#roles` so role sub-pages' `navigate(SettingsPath.Roles)` lands on the Members page with the Roles tab pre-selected. - All affected breadcrumbs updated to nest under their new parents. - `SettingsPath.Roles` and friends now nest under `members/`; `Subdomain` and `CustomDomain` under `general/`; `PublicDomain` and `EmailingDomain` under `applications/`. ## Test plan - [x] `nx typecheck twenty-front` passes - [x] `nx typecheck twenty-server` passes - [x] `oxlint --type-aware` clean on all touched files - [x] `prettier --check` clean on all touched files - [x] Migration applied locally; `publicDomain.applicationId` (uuid, nullable) confirmed in DB - [x] GraphQL schema exposes `PublicDomain.applicationId`, `createPublicDomain.applicationId`, `updatePublicDomain` mutation - [x] **End-to-end route resolution scenarios verified locally:** - Domain bound to App A, function in App A → route matches ✅ - Domain bound to App B, function in App A → route does NOT match (HTTP 404 `TRIGGER_NOT_FOUND`) ✅ - Domain unbound (`applicationId = NULL`) → route matches workspace-wide ✅ - Unknown path on bound domain → returns 404 cleanly ✅ - [x] UI sanity (browser-tested at `apple.localhost:3001`): - General page shows Workspace Domain card - Members page shows Team / Access / Roles tabs - Access tab combines Invite by link + by email + Approved Domains - Roles tab embeds the role list - `/settings/members/roles` direct URL → redirects + Roles tab pre-selected - Apps Developer tab shows Emailing Domains + Public Domains sections - Public Domain detail page has Application picker dropdown listing workspace apps - Sidebar nav: "Domains" and "Roles" no longer present (now folded into General/Members) ## Notes for reviewers - Creating a public domain via the UI still requires Cloudflare credentials in the dev `.env` (`CLOUDFLARE_API_KEY`, `CLOUDFLARE_PUBLIC_DOMAIN_ZONE_ID`, `PUBLIC_DOMAIN_URL`). The DNS step is unchanged from main. - The `applicationId` column is nullable, so existing public-domain rows continue to work workspace-wide — no data backfill required. - `SettingsRolesContainer` was deleted (no longer referenced after `SettingsRoles` index page was removed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
4.5 KiB
Plaintext
142 lines
4.5 KiB
Plaintext
---
|
|
title: APIs
|
|
description: Query and modify your CRM data programmatically using REST or GraphQL.
|
|
---
|
|
|
|
import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
|
|
|
Twenty was built to be developer-friendly, offering powerful APIs that adapt to your custom data model. We provide four distinct API types to meet different integration needs.
|
|
|
|
## Developer-First Approach
|
|
|
|
Twenty generates APIs specifically for your data model:
|
|
- **No long IDs required**: Use your object and field names directly in endpoints
|
|
- **Standard and custom objects treated equally**: Your custom objects get the same API treatment as built-in ones
|
|
- **Dedicated endpoints**: Each object and field gets its own API endpoint
|
|
- **Custom documentation**: Generated specifically for your workspace's data model
|
|
|
|
<Note>
|
|
Your personalized API documentation is available under **Settings → API & Webhooks** after creating an API key. Since Twenty generates APIs that match your custom data model, the documentation is unique to your workspace.
|
|
</Note>
|
|
|
|
## The Two API Types
|
|
|
|
### Core API
|
|
Accessed on `/rest/` or `/graphql/`
|
|
|
|
Work with your actual **records** (the data):
|
|
- Create, read, update, delete People, Companies, Opportunities, etc.
|
|
- Query and filter data
|
|
- Manage record relationships
|
|
|
|
### Metadata API
|
|
Accessed on `/rest/metadata/` or `/metadata/`
|
|
|
|
Manage your **workspace and data model**:
|
|
- Create, modify, or delete objects and fields
|
|
- Configure workspace settings
|
|
- Define relationships between objects
|
|
|
|
## REST vs GraphQL
|
|
|
|
Both Core and Metadata APIs are available in REST and GraphQL formats:
|
|
|
|
| Format | Available Operations |
|
|
|--------|---------------------|
|
|
| **REST** | CRUD, batch operations, upserts |
|
|
| **GraphQL** | Same + **batch upserts**, relationship queries in one call |
|
|
|
|
Choose based on your needs — both formats access the same data.
|
|
|
|
## API Endpoints
|
|
|
|
| Environment | Base URL |
|
|
|-------------|----------|
|
|
| **Cloud** | `https://api.twenty.com/` |
|
|
| **Self-Hosted** | `https://{your-domain}/` |
|
|
|
|
## Authentication
|
|
|
|
Every API request requires an API key in the header:
|
|
|
|
```
|
|
Authorization: Bearer YOUR_API_KEY
|
|
```
|
|
|
|
### Create an API Key
|
|
|
|
1. Go to **Settings → APIs & Webhooks**
|
|
2. Click **+ Create key**
|
|
3. Configure:
|
|
- **Name**: Descriptive name for the key
|
|
- **Expiration Date**: When the key expires
|
|
4. Click **Save**
|
|
5. **Copy immediately** — the key is only shown once
|
|
|
|
<VimeoEmbed videoId="928786722" title="Creating API key" />
|
|
|
|
<Warning>
|
|
Your API key grants access to sensitive data. Don't share it with untrusted services. If compromised, disable it immediately and generate a new one.
|
|
</Warning>
|
|
|
|
### Assign a Role to an API Key
|
|
|
|
For better security, assign a specific role to limit access:
|
|
|
|
1. Go to **Settings → Members → Roles**
|
|
2. Click on the role to assign
|
|
3. Open the **Assignment** tab
|
|
4. Under **API Keys**, click **+ Assign to API key**
|
|
5. Select the API key
|
|
|
|
The key will inherit that role's permissions. See [Permissions](/user-guide/permissions-access/capabilities/permissions) for details.
|
|
|
|
### Manage API Keys
|
|
|
|
**Regenerate**: Settings → APIs & Webhooks → Click key → **Regenerate**
|
|
|
|
**Delete**: Settings → APIs & Webhooks → Click key → **Delete**
|
|
|
|
## API Playground
|
|
|
|
Test your APIs directly in the browser with our built-in playground — available for both **REST** and **GraphQL**.
|
|
|
|
### Access the Playground
|
|
|
|
1. Go to **Settings → APIs & Webhooks**
|
|
2. Create an API key (required)
|
|
3. Click on **REST API** or **GraphQL API** to open the playground
|
|
|
|
### What You Get
|
|
|
|
- **Interactive documentation**: Generated for your specific data model
|
|
- **Live testing**: Execute real API calls against your workspace
|
|
- **Schema explorer**: Browse available objects, fields, and relationships
|
|
- **Request builder**: Construct queries with autocomplete
|
|
|
|
The playground reflects your custom objects and fields, so documentation is always accurate for your workspace.
|
|
|
|
## Batch Operations
|
|
|
|
Both REST and GraphQL support batch operations:
|
|
- **Batch size**: Up to 60 records per request
|
|
- **Operations**: Create, update, delete multiple records
|
|
|
|
**GraphQL-only features:**
|
|
- **Batch Upsert**: Create or update in one call
|
|
- Use plural object names (e.g., `CreateCompanies` instead of `CreateCompany`)
|
|
|
|
## Rate Limits
|
|
|
|
API requests are throttled to ensure platform stability:
|
|
|
|
| Limit | Value |
|
|
|-------|-------|
|
|
| **Requests** | 100 calls per minute |
|
|
| **Batch size** | 60 records per call |
|
|
|
|
<Tip>
|
|
Use batch operations to maximize throughput — process up to 60 records in a single API call instead of making individual requests.
|
|
</Tip>
|
|
|