PJT AI REST API
A standard REST API for integrating PJT AI data with external systems. All requests and responses are JSON, and the base URL is https://api.pjt.ai/api/external/v1.
- Authentication — X-API-Key header (account API key, pjt_ prefix)
- Key scopes — read (read-only, default) / write. POST·PUT·DELETE require a write-scoped key — otherwise 403 SCOPE_FORBIDDEN
- Rate limit — 60 req/min + 10,000 req/month per key (Enterprise by arrangement)
- Response codes — 2xx success, 4xx client error, 5xx server error
- Dates — all timestamps are ISO 8601 (UTC)
Authentication
Every request requires the X-API-Key: <API_KEY> header. Issue keys under Account Settings > API keys (shown only once at creation).
/meMy API key (connection test)
Returns the API key's validity and capabilities (scopes). Being a GET, it needs no write scope — even a read-only key can inspect its own capabilities. A missing/invalid/expired key returns 401, which is itself the connection-test result. Use canProvision/canWrite/canRead in the response to check ability up front (e.g. if canProvision=false, warn before a provisioning call → avoids a false green light). Separate from infrastructure liveness (whether the server is up).
The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.
UNAUTHORIZEDX-API-Key missing, invalid, or expired — this 401 itself is the 'not connected' signal (200 means connected).{
"timestamp": "2026-07-14T09:00:00Z",
"message": "API key is missing, invalid, or expired."
}/provisioning/workspacesEnterprise onlyProvision workspace (idempotent)
Provision a tenant and workspace in one call with a provisioning-scoped API key. Send tenantId to use that tenant (the calling key's account must be OWNER/ADMIN); otherwise tenantExternalRef idempotently finds/creates a tenant in the account namespace (400 if neither is given). workspaceExternalRef is the required idempotency key — repeat requests return the existing resource as 200 instead of creating (201); createdTenant/createdWorkspace tell them apart. Scope hierarchy: provisioning ⊃ write (no separate write needed). ⚠️ businessType has no effect on the result yet (audit only); defaultLocale/accentHue/template are loosely validated (send exact allowed values; unknown template is ignored); response tenant.myRole may be null — do not use it for authorization.
This endpoint is available only on the Enterprise plan. Issuing and using provisioning-scope keys is included in an Enterprise contract.
| Name | Type | Description |
|---|---|---|
| tenantId | integer | Existing tenant ID (Long, optional). When present, tenant·tenantExternalRef are ignored. The calling key's account must be OWNER/ADMIN of that tenant |
| tenantExternalRef | string | New-tenant idempotency key (≤100, used when tenantId is absent). Same (account, value) reuses the existing tenant |
| tenant.name | string | New tenant name (≤100, falls back to workspace.name if omitted) |
| tenant.slug | string | New tenant slug (≤50, globally unique·auto-generated if omitted) |
| tenant.description | string | New tenant description (≤500) |
| workspaceExternalRefREQUIRED | string | Workspace idempotency key (≤100, required). Same (tenantId, value) reuses the existing workspace |
| workspace.nameREQUIRED | string | Workspace name (≤100, required) |
| workspace.slug | string | Workspace slug (≤50, unique within the tenant·derived from name if omitted) |
| workspace.description | string | Workspace description (≤500) |
| workspace.defaultLocale | string | Default locale — ko|en|ja|zh|zh-TW|es|vi|th (not validated; send exact value) |
| workspace.accentHue | integer | Accent hue (0–360, not validated) |
| workspace.template | string | Preset — BLANK|DEV|AGENCY|OPS (unknown values silently ignored) |
| workspace.businessType | string | Business type (forward-looking) — audit only; not reflected in the created workspace |
The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.
SCOPE_FORBIDDENWrite attempted with a read-only key (no write scope) — blocked by the gateway filter{
"error": "SCOPE_FORBIDDEN",
"message": "This API key is read-only. A 'write' scope is required for this operation."
}FORBIDDENKey without the provisioning scope — this endpoint requires provisioning (write alone is insufficient){
"timestamp": "2026-07-14T09:00:00Z",
"message": "The 'provisioning' scope is required."
}FORBIDDENWhen an existing tenantId is given, the calling account is not OWNER/ADMIN (or not a member) of that tenant{
"timestamp": "2026-07-14T09:00:00Z",
"message": "OWNER or ADMIN role on the tenant is required."
}BAD_REQUESTNeither tenantId nor tenantExternalRef was provided{
"timestamp": "2026-07-14T09:00:00Z",
"message": "One of tenantId or tenantExternalRef is required."
}VALIDATION_ERRORValidation failed (required/length, etc.) — per-field details in errors[]{
"timestamp": "2026-07-14T09:00:00Z",
"code": "VALIDATION_ERROR",
"message": "workspaceExternalRef: must not be blank",
"errors": [
{ "field": "workspaceExternalRef", "code": "NotBlank", "message": "must not be blank" }
]
}UNAUTHORIZEDX-API-Key missing, invalid, or expired (common to all External endpoints){
"timestamp": "2026-07-14T09:00:00Z",
"message": "API key is missing, invalid, or expired."
}/tenantsMy tenants
Returns every tenant the API key's account belongs to (direct tenant members + organizations reachable only via a workspace = MEMBER). It's a GET, so a read scope is enough. Judge permissions from each item's myRole (OWNER|ADMIN|MEMBER) — creating a workspace (POST /workspaces) is only possible on tenants where myRole ∈ [OWNER, ADMIN], so an SI client picks an OWNER/ADMIN item's id here and uses it as tenantId.
| Name | Type | Description |
|---|---|---|
| id | Long | Tenant ID — use as tenantId in POST /workspaces |
| myRole | String (enum) | OWNER | ADMIN | MEMBER (null if not a member). Workspace creation (POST /workspaces) is allowed only on tenants where you are OWNER or ADMIN |
| status | String (enum) | Tenant status — ACTIVE | ARCHIVED | DELETE (Tenant.Status) |
| slug | String | Organization slug (used for routing) |
| mfaSetupRequired | boolean | true if the organization enforces 2FA and it is not yet set up |
| slugSet | boolean | Whether the user set the slug explicitly (false for auto-generated org-xxxx) |
The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.
UNAUTHORIZEDX-API-Key missing, invalid, or expired (common to all External endpoints){
"timestamp": "2026-07-14T20:00:00Z",
"message": "API key is missing, invalid, or expired."
}/tenants/{tenantId}Single tenant
Returns one tenant in the same shape as the list. If you don't belong to it, the access check fails (403); a nonexistent tenant returns 400 tenant.not_found. A read scope is enough.
| Name | Type | Description |
|---|---|---|
| tenantIdREQUIRED | integer | Tenant ID to fetch (Long, required) |
The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.
FORBIDDENThe calling account is not a member of that tenant (tenant access check failed){
"timestamp": "2026-07-14T20:00:00Z",
"message": "테넌트 멤버가 아닙니다"
}BAD_REQUESTNonexistent tenant (tenant.not_found){
"timestamp": "2026-07-14T20:00:00Z",
"message": "테넌트를 찾을 수 없습니다"
}UNAUTHORIZEDX-API-Key missing, invalid, or expired (common to all External endpoints){
"timestamp": "2026-07-14T20:00:00Z",
"message": "API key is missing, invalid, or expired."
}/workspacesCreate workspace (existing tenant)
A lightweight path separate from provisioning — it does NOT create a tenant; it creates only a workspace under an existing tenant (tenantId). A write scope is enough (provisioning not required; read-only keys get 403 SCOPE_FORBIDDEN). The calling key's account must be OWNER/ADMIN of that tenant. If you pass externalRef it is idempotent — repeating with the same (tenantId, externalRef) returns the existing workspace as 200 instead of creating a new one (201); if omitted, a new workspace is created on every call (passing it is recommended for safe retries). The created workspace is a team workspace (not personal). ⚠️ defaultLocale/accentHue/template are weakly validated (values pass through; an unrecognized template is ignored). If you need the tenant auto-created, use provisioning (POST /provisioning/workspaces).
| Name | Type | Description |
|---|---|---|
| tenantIdREQUIRED | integer | Create under this tenant (Long, required). The calling key's account must be OWNER/ADMIN of it |
| externalRef | string | Idempotency key (≤100, optional). Repeating with the same (tenantId, value) returns the existing workspace. Omit → a new workspace on every call |
| nameREQUIRED | string | Workspace name (≤100, required) |
| slug | string | Workspace slug (≤50, unique within the tenant·derived from name if omitted) |
| description | string | Workspace description (≤500) |
| defaultLocale | string | Default locale — ko|en|ja|zh|zh-TW|es|vi|th (passes unvalidated; send an exact value) |
| accentHue | integer | Accent hue (0–360, passes unvalidated) |
| template | string | Preset — BLANK|DEV|AGENCY|OPS (unrecognized values are silently ignored) |
The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.
SCOPE_FORBIDDENWrite attempted with a read-only key (no write scope) — blocked by the gateway filter{
"error": "SCOPE_FORBIDDEN",
"message": "This API key is read-only. A 'write' scope is required for this operation."
}FORBIDDENThe calling account is not OWNER/ADMIN of the tenantId tenant (tenant.admin_required){
"timestamp": "2026-07-14T20:00:00Z",
"message": "관리자 권한이 필요합니다"
}FORBIDDENThe calling account is not a member of that tenant (including nonexistent tenants) (tenant.not_member){
"timestamp": "2026-07-14T20:00:00Z",
"message": "테넌트 멤버가 아닙니다"
}VALIDATION_ERRORValidation failed (missing tenantId/name, etc.) — per-field details in errors[]{
"timestamp": "2026-07-14T20:00:00Z",
"code": "VALIDATION_ERROR",
"message": "tenantId: must not be null",
"errors": [
{ "field": "tenantId", "code": "NotNull", "message": "must not be null" }
]
}UNAUTHORIZEDX-API-Key missing, invalid, or expired (common to all External endpoints){
"timestamp": "2026-07-14T20:00:00Z",
"message": "API key is missing, invalid, or expired."
}/workspaces/slug/availableCheck workspace slug availability
Checks whether a workspace slug is available (not taken) within a tenant. Because a slug is unique per (tenantId, slug), tenantId is required just like workspace creation. The slug is normalized the same way as on save (trimmed, lowercased) before comparison, and an empty or duplicate value returns available:false. Authorization uses the same gate as workspace creation — the caller must be an OWNER/ADMIN of that tenant (requireTenantAdmin); non-members and non-admins get 403, and tenant existence is not disclosed. Being a GET, a read scope is sufficient. Use this to pre-check slug availability before creating a workspace (POST /workspaces).
| Name | Type | Description |
|---|---|---|
| tenantIdREQUIRED | integer | Tenant scope for the uniqueness check (Long, required query). The slug uniqueness scope. The caller must be an OWNER/ADMIN of this tenant |
| slugREQUIRED | string | Workspace slug to check (query, required). Compared after normalization (trim, lowercase) |
| Name | Type | Description |
|---|---|---|
| available | boolean | true = available (not taken), false = already in use or empty slug |
The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.
FORBIDDENCaller is not an OWNER/ADMIN of the tenantId tenant (including non-members) — blocked by requireTenantAdmin (tenant existence not disclosed){
"timestamp": "2026-07-14T20:00:00Z",
"message": "관리자 권한이 필요합니다"
}UNAUTHORIZEDX-API-Key missing, invalid, or expired (common to External API){
"timestamp": "2026-07-14T20:00:00Z",
"message": "API key is missing, invalid, or expired."
}/workspacesList workspaces
Returns the workspaces accessible to the API-key account.
/workspaces/{workspaceId}Get workspace
Returns a single workspace.
| Name | Type | Description |
|---|---|---|
| workspaceIdREQUIRED | integer | Workspace ID |
/projectsList projects by tenant & workspace
Returns the projects of the given tenant and workspace, paginated. tenantId and workspaceId are required; status can further filter.
| Name | Type | Description |
|---|---|---|
| tenantIdREQUIRED | integer | Tenant ID (required) |
| workspaceIdREQUIRED | integer | Workspace ID (required) |
| status | enum | Exact match on project status (PLANNING, ESTIMATING, WAITING, IN_PROGRESS, ON_HOLD, COMPLETED, CANCELLED). When set, cancelled/archived are also included. |
| page | integer | Page number (0-based) |
| size | integer | Page size (default 20, max 100) |
The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.
BAD_REQUESTWorkspace does not exist or does not belong to the tenant (cross-tenant is returned as not_found without leaking existence).{
"timestamp": "2026-07-14T09:00:00Z",
"message": "워크스페이스를 찾을 수 없습니다"
}BAD_REQUESTAn unsupported value was passed to status.{
"timestamp": "2026-07-14T09:00:00Z",
"message": "유효하지 않은 프로젝트 상태 값입니다"
}FORBIDDENNo access to this workspace (not an ACTIVE member nor an accepted client/partner — no tenant-membership fallback).{
"timestamp": "2026-07-14T09:00:00Z",
"message": "워크스페이스 접근 권한이 없습니다"
}UNAUTHORIZEDX-API-Key missing, invalid, or expired.{
"timestamp": "2026-07-14T09:00:00Z",
"message": "API key is missing, invalid, or expired."
}/workspaces/{workspaceId}/projectsList projects
Returns the active projects of a workspace (cancelled/archived excluded).
| Name | Type | Description |
|---|---|---|
| workspaceIdREQUIRED | integer | Workspace ID |
/projectsCreate project (idempotent)
Creates a project. With externalRef the call is idempotent — repeating the same (workspaceId, externalRef) returns the existing project with 200 instead of creating a new one (201 on first creation).
| Name | Type | Description |
|---|---|---|
| workspaceIdREQUIRED | integer | Workspace ID |
| nameREQUIRED | string | Project name |
| externalRef | string | Idempotency key — repeating with the same value returns the existing project with 200 instead of creating a new one |
| code | string | Project code (task key prefix). Auto-generated from the name if omitted |
| description | string | Description |
| clientIds | array | Array of client IDs |
| managerId | integer | Manager account ID |
| startDate | date | Start date — ISO 8601 (YYYY-MM-DD) |
| endDate | date | End date — ISO 8601 (YYYY-MM-DD) |
| budget | integer | Budget |
/projects/{projectId}Get project / progress
Returns project detail including status, progress (%), planned/actual dates and last-modified time.
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | string | Project ID — numeric ID or publicId starting with p_ |
/projects/{projectId}Update project
Updates only the fields you send (partial update).
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | string | Project ID — numeric ID or publicId starting with p_ |
| Name | Type | Description |
|---|---|---|
| name | string | Project name |
| status | enum | Status — PLANNING·ESTIMATING·WAITING·IN_PROGRESS·ON_HOLD·COMPLETED·CANCELLED |
| progressRate | integer | Progress (%) 0–100 |
| startDate | date | Start date — ISO 8601 (YYYY-MM-DD) |
| endDate | date | End date — ISO 8601 (YYYY-MM-DD) |
/projects/{projectId}Delete project
Deletes a project. Returns 204 No Content on success (empty body).
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | string | Project ID — numeric ID or publicId starting with p_ |
/projects/{projectId}/timelineTimeline (milestones)
Project milestones — name, status (PLANNED/IN_PROGRESS/COMPLETED), due date, completion date and progress. Ordered by sortOrder.
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | string | Project ID — numeric ID or publicId starting with p_ |
/projects/{projectId}/activitiesActivities
Task change events of the project, newest first — type, message, actor and timestamp.
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | string | Project ID — numeric ID or publicId starting with p_ |
| Name | Type | Description |
|---|---|---|
| page | integer | Page number (0-based, default 0) |
| size | integer | Page size (default 50, max 200) |
/projects/{projectId}/filesFiles
Task and comment attachments of the project merged, newest first. fileUrl is a static, non-expiring URL.
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | string | Project ID — numeric ID or publicId starting with p_ |
| Name | Type | Description |
|---|---|---|
| limit | integer | Maximum items (default 100, max 500) |
/projects/{projectId}/tasksList tasks
Returns the tasks of a project. Use /tasks/paged when you need pagination.
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | string | Project ID — numeric ID or publicId starting with p_ |
| Name | Type | Description |
|---|---|---|
| sortBy | string | Sort field (default createdAt) |
/tasksCreate task
Creates a task.
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | integer | Project ID (numeric) |
| titleREQUIRED | string | Title |
| description | string | Description |
| assigneeId | integer | Assignee account ID |
| status | enum | Status — PENDING·TODO·IN_PROGRESS·IN_REVIEW·BLOCKED·COMPLETED·CANCELLED |
| priority | enum | Priority — URGENT·HIGH·MEDIUM·LOW |
| dueDate | date | Due date — ISO 8601 |
| milestoneId | integer | Milestone ID to link |
/tasks/{taskId}Get task
Returns a single task.
| Name | Type | Description |
|---|---|---|
| taskIdREQUIRED | integer | Task ID |
/tasks/{taskId}Update task
Updates only the fields you send.
| Name | Type | Description |
|---|---|---|
| taskIdREQUIRED | integer | Task ID |
| Name | Type | Description |
|---|---|---|
| title | string | Title |
| status | enum | Status — PENDING·TODO·IN_PROGRESS·IN_REVIEW·BLOCKED·COMPLETED·CANCELLED |
| priority | enum | Priority — URGENT·HIGH·MEDIUM·LOW |
| dueDate | date | Due date — ISO 8601 |
/tasks/{taskId}Delete task
Deletes a task. Returns 204 No Content on success.
| Name | Type | Description |
|---|---|---|
| taskIdREQUIRED | integer | Task ID |
/tasks/assignedMy assigned tasks
Returns the tasks assigned to the API-key account.
/projects/{projectId}/documentsList documents
Returns the documents of a project. Use /documents/paged when you need pagination.
| Name | Type | Description |
|---|---|---|
| projectIdREQUIRED | string | Project ID — numeric ID or publicId starting with p_ |
/documents/{documentId}Get document
Returns a single document including its content.
| Name | Type | Description |
|---|---|---|
| documentIdREQUIRED | integer | Document ID |
/documentsCreate document
Creates a document.
| Name | Type | Description |
|---|---|---|
| workspaceIdREQUIRED | integer | Workspace ID |
| titleREQUIRED | string | Title |
| documentTypeREQUIRED | enum | Document type (e.g. REQUIREMENT, MEETING_NOTE) |
| projectId | integer | Project ID (numeric) |
| content | string | Document body |
| visibility | enum | Visibility — PUBLIC·TEAM·PRIVATE |
/documents/{documentId}Update document
Updates only the fields you send.
| Name | Type | Description |
|---|---|---|
| documentIdREQUIRED | integer | Document ID |
| Name | Type | Description |
|---|---|---|
| title | string | Title |
| content | string | Document body |
| visibility | enum | Visibility — PUBLIC·TEAM·PRIVATE |
/documents/{documentId}Delete document
Deletes a document. Returns 204 No Content on success.
| Name | Type | Description |
|---|---|---|
| documentIdREQUIRED | integer | Document ID |
Error codes
Every error response includes error.code and error.message.
| Code | Name | Description | Action |
|---|---|---|---|
| 400 | Bad Request | The request body is invalid. | Validate the request body |
| 401 | Unauthorized | The API key is invalid or missing. | Re-check the API key |
| 403 | Forbidden | You don't have permission to access this resource. Write requests with a read-only key return SCOPE_FORBIDDEN. | Check roles/scopes |
| 404 | Not Found | The requested resource was not found. | Re-check the ID |
| 429 | Rate Limited | You exceeded the rate limit. | See the Retry-After header and back off |
| 500 | Server Error | An error occurred while the server processed the request. | Retry after 5 min, check status.pjt.ai |