Files
twenty/packages/twenty-docs/developers/extend/capabilities/apis.mdx
T
34b927ff23 feat(public-domain): bind public domains to apps + reorganize settings (#20360)
## 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>
2026-05-10 20:17:28 +02:00

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>