PJT AIPJT AI/API REFERENCE
v1https://api.pjt.ai/api/external/v1
Getting startedMCP
OverviewAuthenticationError codes
Account
  • GET/me
Provisioning
  • POST/provisioning/workspacesENT
Tenants
  • GET/tenants
  • GET/tenants/{tenantId}
Workspaces
  • POST/workspaces
  • GET/workspaces/slug/available
  • GET/workspaces
  • GET/workspaces/{workspaceId}
Projects
  • GET/projects
  • GET/workspaces/{workspaceId}/projects
  • POST/projects
  • GET/projects/{projectId}
  • PUT/projects/{projectId}
  • DELETE/projects/{projectId}
  • GET/projects/{projectId}/timeline
  • GET/projects/{projectId}/activities
  • GET/projects/{projectId}/files
Tasks
  • GET/projects/{projectId}/tasks
  • POST/tasks
  • GET/tasks/{taskId}
  • PUT/tasks/{taskId}
  • DELETE/tasks/{taskId}
  • GET/tasks/assigned
Documents
  • GET/projects/{projectId}/documents
  • GET/documents/{documentId}
  • POST/documents
  • PUT/documents/{documentId}
  • DELETE/documents/{documentId}
OVERVIEW

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.

Basics
  • 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)
AUTH

Authentication

Every request requires the X-API-Key: <API_KEY> header. Issue keys under Account Settings > API keys (shown only once at creation).

⚠
Storing keys
Use API keys only on the server side. If one is exposed to a client (browser or mobile app), revoke and reissue it immediately.
🔑
Key permissions
An API key acts with the permissions of the account that issued it. Creating under an existing tenant (tenantId) requires that account to be OWNER/ADMIN of the tenant (otherwise 403); when a new tenant is created, that account becomes its OWNER. Issuing a provisioning-scoped key is restricted to SUPER_ADMIN.
Account
GET/me

My 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).

Response codes
200OK401Unauthorized
Error responses

The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.

401UNAUTHORIZEDX-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
POST/provisioning/workspacesEnterprise only

Provision 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.

Body parameters
NameTypeDescription
tenantIdintegerExisting tenant ID (Long, optional). When present, tenant·tenantExternalRef are ignored. The calling key's account must be OWNER/ADMIN of that tenant
tenantExternalRefstringNew-tenant idempotency key (≤100, used when tenantId is absent). Same (account, value) reuses the existing tenant
tenant.namestringNew tenant name (≤100, falls back to workspace.name if omitted)
tenant.slugstringNew tenant slug (≤50, globally unique·auto-generated if omitted)
tenant.descriptionstringNew tenant description (≤500)
workspaceExternalRefREQUIREDstringWorkspace idempotency key (≤100, required). Same (tenantId, value) reuses the existing workspace
workspace.nameREQUIREDstringWorkspace name (≤100, required)
workspace.slugstringWorkspace slug (≤50, unique within the tenant·derived from name if omitted)
workspace.descriptionstringWorkspace description (≤500)
workspace.defaultLocalestringDefault locale — ko|en|ja|zh|zh-TW|es|vi|th (not validated; send exact value)
workspace.accentHueintegerAccent hue (0–360, not validated)
workspace.templatestringPreset — BLANK|DEV|AGENCY|OPS (unknown values silently ignored)
workspace.businessTypestringBusiness type (forward-looking) — audit only; not reflected in the created workspace
Response codes
200OK201Created400Bad Request401Unauthorized403Forbidden429Rate Limited
Error responses

The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.

403SCOPE_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."
}
403FORBIDDENKey without the provisioning scope — this endpoint requires provisioning (write alone is insufficient)
{
  "timestamp": "2026-07-14T09:00:00Z",
  "message": "The 'provisioning' scope is required."
}
403FORBIDDENWhen 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."
}
400BAD_REQUESTNeither tenantId nor tenantExternalRef was provided
{
  "timestamp": "2026-07-14T09:00:00Z",
  "message": "One of tenantId or tenantExternalRef is required."
}
400VALIDATION_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" }
  ]
}
401UNAUTHORIZEDX-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."
}
Tenants
GET/tenants

My 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.

Response fields
NameTypeDescription
idLongTenant ID — use as tenantId in POST /workspaces
myRoleString (enum)OWNER | ADMIN | MEMBER (null if not a member). Workspace creation (POST /workspaces) is allowed only on tenants where you are OWNER or ADMIN
statusString (enum)Tenant status — ACTIVE | ARCHIVED | DELETE (Tenant.Status)
slugStringOrganization slug (used for routing)
mfaSetupRequiredbooleantrue if the organization enforces 2FA and it is not yet set up
slugSetbooleanWhether the user set the slug explicitly (false for auto-generated org-xxxx)
Response codes
200OK401Unauthorized
Error responses

The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.

401UNAUTHORIZEDX-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."
}
GET/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.

Path parameters
NameTypeDescription
tenantIdREQUIREDintegerTenant ID to fetch (Long, required)
Response codes
200OK400Bad Request401Unauthorized403Forbidden
Error responses

The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.

403FORBIDDENThe calling account is not a member of that tenant (tenant access check failed)
{
  "timestamp": "2026-07-14T20:00:00Z",
  "message": "테넌트 멤버가 아닙니다"
}
400BAD_REQUESTNonexistent tenant (tenant.not_found)
{
  "timestamp": "2026-07-14T20:00:00Z",
  "message": "테넌트를 찾을 수 없습니다"
}
401UNAUTHORIZEDX-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
POST/workspaces

Create 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).

Body parameters
NameTypeDescription
tenantIdREQUIREDintegerCreate under this tenant (Long, required). The calling key's account must be OWNER/ADMIN of it
externalRefstringIdempotency key (≤100, optional). Repeating with the same (tenantId, value) returns the existing workspace. Omit → a new workspace on every call
nameREQUIREDstringWorkspace name (≤100, required)
slugstringWorkspace slug (≤50, unique within the tenant·derived from name if omitted)
descriptionstringWorkspace description (≤500)
defaultLocalestringDefault locale — ko|en|ja|zh|zh-TW|es|vi|th (passes unvalidated; send an exact value)
accentHueintegerAccent hue (0–360, passes unvalidated)
templatestringPreset — BLANK|DEV|AGENCY|OPS (unrecognized values are silently ignored)
Response codes
201Created200OK400Bad Request401Unauthorized403Forbidden
Error responses

The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.

403SCOPE_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."
}
403FORBIDDENThe calling account is not OWNER/ADMIN of the tenantId tenant (tenant.admin_required)
{
  "timestamp": "2026-07-14T20:00:00Z",
  "message": "관리자 권한이 필요합니다"
}
403FORBIDDENThe calling account is not a member of that tenant (including nonexistent tenants) (tenant.not_member)
{
  "timestamp": "2026-07-14T20:00:00Z",
  "message": "테넌트 멤버가 아닙니다"
}
400VALIDATION_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" }
  ]
}
401UNAUTHORIZEDX-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."
}
GET/workspaces/slug/available

Check 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).

Query parameters
NameTypeDescription
tenantIdREQUIREDintegerTenant scope for the uniqueness check (Long, required query). The slug uniqueness scope. The caller must be an OWNER/ADMIN of this tenant
slugREQUIREDstringWorkspace slug to check (query, required). Compared after normalization (trim, lowercase)
Response fields
NameTypeDescription
availablebooleantrue = available (not taken), false = already in use or empty slug
Response codes
200OK401Unauthorized403Forbidden
Error responses

The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.

403FORBIDDENCaller 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": "관리자 권한이 필요합니다"
}
401UNAUTHORIZEDX-API-Key missing, invalid, or expired (common to External API)
{
  "timestamp": "2026-07-14T20:00:00Z",
  "message": "API key is missing, invalid, or expired."
}
GET/workspaces

List workspaces

Returns the workspaces accessible to the API-key account.

Response codes
200OK401Unauthorized429Rate Limited
GET/workspaces/{workspaceId}

Get workspace

Returns a single workspace.

Path parameters
NameTypeDescription
workspaceIdREQUIREDintegerWorkspace ID
Response codes
200OK401Unauthorized404Not Found
Projects
GET/projects

List projects by tenant & workspace

Returns the projects of the given tenant and workspace, paginated. tenantId and workspaceId are required; status can further filter.

Query parameters
NameTypeDescription
tenantIdREQUIREDintegerTenant ID (required)
workspaceIdREQUIREDintegerWorkspace ID (required)
statusenumExact match on project status (PLANNING, ESTIMATING, WAITING, IN_PROGRESS, ON_HOLD, COMPLETED, CANCELLED). When set, cancelled/archived are also included.
pageintegerPage number (0-based)
sizeintegerPage size (default 20, max 100)
Response codes
200OK400Bad Request401Unauthorized403Forbidden429Rate Limited
Error responses

The message field is returned in the request locale (?lang or Accept-Language) across 8 locales.

400BAD_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": "워크스페이스를 찾을 수 없습니다"
}
400BAD_REQUESTAn unsupported value was passed to status.
{
  "timestamp": "2026-07-14T09:00:00Z",
  "message": "유효하지 않은 프로젝트 상태 값입니다"
}
403FORBIDDENNo access to this workspace (not an ACTIVE member nor an accepted client/partner — no tenant-membership fallback).
{
  "timestamp": "2026-07-14T09:00:00Z",
  "message": "워크스페이스 접근 권한이 없습니다"
}
401UNAUTHORIZEDX-API-Key missing, invalid, or expired.
{
  "timestamp": "2026-07-14T09:00:00Z",
  "message": "API key is missing, invalid, or expired."
}
GET/workspaces/{workspaceId}/projects

List projects

Returns the active projects of a workspace (cancelled/archived excluded).

Path parameters
NameTypeDescription
workspaceIdREQUIREDintegerWorkspace ID
Response codes
200OK401Unauthorized403Forbidden
POST/projects

Create 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).

Body parameters
NameTypeDescription
workspaceIdREQUIREDintegerWorkspace ID
nameREQUIREDstringProject name
externalRefstringIdempotency key — repeating with the same value returns the existing project with 200 instead of creating a new one
codestringProject code (task key prefix). Auto-generated from the name if omitted
descriptionstringDescription
clientIdsarrayArray of client IDs
managerIdintegerManager account ID
startDatedateStart date — ISO 8601 (YYYY-MM-DD)
endDatedateEnd date — ISO 8601 (YYYY-MM-DD)
budgetintegerBudget
Response codes
201Created200OK400Bad Request401Unauthorized
GET/projects/{projectId}

Get project / progress

Returns project detail including status, progress (%), planned/actual dates and last-modified time.

Path parameters
NameTypeDescription
projectIdREQUIREDstringProject ID — numeric ID or publicId starting with p_
Response codes
200OK404Not Found
PUT/projects/{projectId}

Update project

Updates only the fields you send (partial update).

Path parameters
NameTypeDescription
projectIdREQUIREDstringProject ID — numeric ID or publicId starting with p_
Body parameters
NameTypeDescription
namestringProject name
statusenumStatus — PLANNING·ESTIMATING·WAITING·IN_PROGRESS·ON_HOLD·COMPLETED·CANCELLED
progressRateintegerProgress (%) 0–100
startDatedateStart date — ISO 8601 (YYYY-MM-DD)
endDatedateEnd date — ISO 8601 (YYYY-MM-DD)
Response codes
200OK400Bad Request404Not Found
DELETE/projects/{projectId}

Delete project

Deletes a project. Returns 204 No Content on success (empty body).

Path parameters
NameTypeDescription
projectIdREQUIREDstringProject ID — numeric ID or publicId starting with p_
Response codes
204No Content403Forbidden404Not Found
GET/projects/{projectId}/timeline

Timeline (milestones)

Project milestones — name, status (PLANNED/IN_PROGRESS/COMPLETED), due date, completion date and progress. Ordered by sortOrder.

Path parameters
NameTypeDescription
projectIdREQUIREDstringProject ID — numeric ID or publicId starting with p_
Response codes
200OK403Forbidden404Not Found
GET/projects/{projectId}/activities

Activities

Task change events of the project, newest first — type, message, actor and timestamp.

Path parameters
NameTypeDescription
projectIdREQUIREDstringProject ID — numeric ID or publicId starting with p_
Query parameters
NameTypeDescription
pageintegerPage number (0-based, default 0)
sizeintegerPage size (default 50, max 200)
Response codes
200OK403Forbidden404Not Found
GET/projects/{projectId}/files

Files

Task and comment attachments of the project merged, newest first. fileUrl is a static, non-expiring URL.

Path parameters
NameTypeDescription
projectIdREQUIREDstringProject ID — numeric ID or publicId starting with p_
Query parameters
NameTypeDescription
limitintegerMaximum items (default 100, max 500)
Response codes
200OK403Forbidden404Not Found
Tasks
GET/projects/{projectId}/tasks

List tasks

Returns the tasks of a project. Use /tasks/paged when you need pagination.

Path parameters
NameTypeDescription
projectIdREQUIREDstringProject ID — numeric ID or publicId starting with p_
Query parameters
NameTypeDescription
sortBystringSort field (default createdAt)
Response codes
200OK403Forbidden404Not Found
POST/tasks

Create task

Creates a task.

Body parameters
NameTypeDescription
projectIdREQUIREDintegerProject ID (numeric)
titleREQUIREDstringTitle
descriptionstringDescription
assigneeIdintegerAssignee account ID
statusenumStatus — PENDING·TODO·IN_PROGRESS·IN_REVIEW·BLOCKED·COMPLETED·CANCELLED
priorityenumPriority — URGENT·HIGH·MEDIUM·LOW
dueDatedateDue date — ISO 8601
milestoneIdintegerMilestone ID to link
Response codes
201Created400Bad Request401Unauthorized
GET/tasks/{taskId}

Get task

Returns a single task.

Path parameters
NameTypeDescription
taskIdREQUIREDintegerTask ID
Response codes
200OK404Not Found
PUT/tasks/{taskId}

Update task

Updates only the fields you send.

Path parameters
NameTypeDescription
taskIdREQUIREDintegerTask ID
Body parameters
NameTypeDescription
titlestringTitle
statusenumStatus — PENDING·TODO·IN_PROGRESS·IN_REVIEW·BLOCKED·COMPLETED·CANCELLED
priorityenumPriority — URGENT·HIGH·MEDIUM·LOW
dueDatedateDue date — ISO 8601
Response codes
200OK400Bad Request404Not Found
DELETE/tasks/{taskId}

Delete task

Deletes a task. Returns 204 No Content on success.

Path parameters
NameTypeDescription
taskIdREQUIREDintegerTask ID
Response codes
204No Content403Forbidden404Not Found
GET/tasks/assigned

My assigned tasks

Returns the tasks assigned to the API-key account.

Response codes
200OK401Unauthorized
Documents
GET/projects/{projectId}/documents

List documents

Returns the documents of a project. Use /documents/paged when you need pagination.

Path parameters
NameTypeDescription
projectIdREQUIREDstringProject ID — numeric ID or publicId starting with p_
Response codes
200OK403Forbidden404Not Found
GET/documents/{documentId}

Get document

Returns a single document including its content.

Path parameters
NameTypeDescription
documentIdREQUIREDintegerDocument ID
Response codes
200OK404Not Found
POST/documents

Create document

Creates a document.

Body parameters
NameTypeDescription
workspaceIdREQUIREDintegerWorkspace ID
titleREQUIREDstringTitle
documentTypeREQUIREDenumDocument type (e.g. REQUIREMENT, MEETING_NOTE)
projectIdintegerProject ID (numeric)
contentstringDocument body
visibilityenumVisibility — PUBLIC·TEAM·PRIVATE
Response codes
201Created400Bad Request401Unauthorized
PUT/documents/{documentId}

Update document

Updates only the fields you send.

Path parameters
NameTypeDescription
documentIdREQUIREDintegerDocument ID
Body parameters
NameTypeDescription
titlestringTitle
contentstringDocument body
visibilityenumVisibility — PUBLIC·TEAM·PRIVATE
Response codes
200OK400Bad Request404Not Found
DELETE/documents/{documentId}

Delete document

Deletes a document. Returns 204 No Content on success.

Path parameters
NameTypeDescription
documentIdREQUIREDintegerDocument ID
Response codes
204No Content403Forbidden404Not Found
ERRORS

Error codes

Every error response includes error.code and error.message.

CodeNameDescriptionAction
400Bad RequestThe request body is invalid.Validate the request body
401UnauthorizedThe API key is invalid or missing.Re-check the API key
403ForbiddenYou don't have permission to access this resource. Write requests with a read-only key return SCOPE_FORBIDDEN.Check roles/scopes
404Not FoundThe requested resource was not found.Re-check the ID
429Rate LimitedYou exceeded the rate limit.See the Retry-After header and back off
500Server ErrorAn error occurred while the server processed the request.Retry after 5 min, check status.pjt.ai
BASE URL
https://api.pjt.ai/api/external/v1
VERSION
v1 · released 2026-07