> ## Documentation Index
> Fetch the complete documentation index at: https://help.statisfy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Portal SDK: Project API

> Read and update a customer's onboarding project — REST endpoints and the React SDK for tasks, fields, and comments.

The Portal SDK lets an embedded portal show a customer their onboarding **project** — its fields, milestones, tasks, and comments — and let them make the changes you allow. It has two layers:

<CardGroup cols={2}>
  <Card title="React SDK" icon="react">
    `@statisfy/digital-workers-react` — drop-in components (`<ProjectModule/>`, `<Comments/>`) and headless hooks (`useProject`, `useComments`).
  </Card>

  <Card title="REST API" icon="code">
    The `/sdk/v1` endpoints the React SDK calls. Use them directly if you're not on React.
  </Card>
</CardGroup>

Everything is scoped to the **customer** in the session token — the portal never sends a customer id, and a project or task that isn't the session's is reported as `404` (existence is never leaked).

## Authentication

Every call needs a **portal session token** (a short-lived JWT) as a bearer, plus your **publishable key**:

```http theme={null}
Authorization: Bearer <portal-session-token>
X-Statisfy-Publishable-Key: <publishable_key>
```

Mint the session token by exchanging the signed-in user's identity at your gateway's token endpoint; the React SDK does this for you via `<StatisfyProvider>`. See [Publishable keys](/admin/publishable_keys) to create a key.

<Note>
  Visibility is enforced server-side. A real (non-preview) external viewer only receives fields, tasks, and comments that have been shared with the customer — see [External sharing](/projects/external_sharing). An internal preview session sees everything.
</Note>

## What the customer can see and edit

* **Fields** marked *Internal* are removed from the response entirely.
* **Tasks** hidden from the portal are dropped from the task list.
* **Comments** that weren't shared with the customer are filtered out; comments the customer posts are always shared.
* Writes are gated by `meta.can_edit` and each field's `editable` flag.

***

## REST API

Base path: `/sdk/v1`. All responses are JSON.

**Error responses:**

| Status                   | Shape                                                                                                                                    |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `400` `401` `403` `404`  | `{ "detail": { "code": "...", "message": "..." } }`                                                                                      |
| `422` (validation error) | `{ "detail": [{ "loc": ["body", "field_name"], "msg": "...", "type": "..." }] }` — `detail` is an **array** of validation-error objects. |

### Resolve the onboarding project

```http theme={null}
GET /sdk/v1/config/portal
```

Returns the customer's onboarding project id under `onboarding_project__c_`, which you then read with `GET /projects/{id}`.

```json theme={null}
{ "data": { "onboarding_project__c_": "1102" }, "meta": { "module": "portal" } }
```

### Read a project

```http theme={null}
GET /sdk/v1/projects/{project_id}
```

Returns the project's values, human-readable display strings, field schema, server-ordered tasks, and a progress meter.

```json theme={null}
{
  "data": {
    "id": "1102",
    "properties": { "title": "New Onboarding", "status": "42", "due_date": "2026-07-04" },
    "display": { "status": "In Progress" },
    "schema": [ { "key": "status", "label": "Status", "type": "select", "editable": false, "options": [] } ],
    "progress": { "percent": 40, "completed": 2, "total": 5 },
    "tasks": [ { "id": "t1", "parent_id": null, "is_milestone": true, "status": "9", "step_status": "current", "properties": {}, "display": {} } ],
    "tasks_schema": [ { "key": "status", "label": "Status", "type": "select", "editable": true, "options": [] } ]
  },
  "meta": { "can_edit": true }
}
```

Render `display[key] ?? properties[key]`; send only canonical `properties` back on writes. `tasks` is a flat, depth-first list — build the milestone → subtask tree client-side from `parent_id`, and never re-sort it.

### Update a project or task

```http theme={null}
PATCH /sdk/v1/projects/{project_id}
PATCH /sdk/v1/projects/{project_id}/tasks/{task_id}
```

Body is the **changed canonical values only**:

```json theme={null}
{ "properties": { "status": "10" } }
```

Both return the refreshed project payload. A non-editable field or invalid option is rejected with a `400` (`not_editable` / `invalid_option` / `invalid_type`).

### Comments

```http theme={null}
GET  /sdk/v1/projects/{project_id}/comments
POST /sdk/v1/projects/{project_id}/comments
GET  /sdk/v1/projects/{project_id}/tasks/{task_id}/comments
POST /sdk/v1/projects/{project_id}/tasks/{task_id}/comments
PATCH /sdk/v1/projects/{project_id}/comments/{comment_id}
```

`GET` returns customer-visible comments oldest-first:

```json theme={null}
{ "data": [ { "id": 7, "value": "Welcome aboard!", "author_id": "p_1", "author_name": "Alex", "created_at": "2026-07-04T10:00:00Z", "updated_at": null, "visibility": "external", "can_edit": false } ] }
```

`POST` takes `{ "value": "..." }` and returns the created comment. Portal-posted comments are always shared with the customer. `PATCH` (author only) edits the text.

| Field        | Meaning                                                                            |
| ------------ | ---------------------------------------------------------------------------------- |
| `visibility` | `external` (shared with the customer) — internal comments are never returned here. |
| `can_edit`   | `true` when the session's person authored it.                                      |

***

## React SDK

Install and wrap your app in `<StatisfyProvider>` (see [Portals overview](/portals/overview)); it supplies the base URL, token minting, and publishable key to every hook and component.

### `<ProjectModule/>`

The pre-assembled project view: header, progress, milestone journey, editable task list, project comments, and a task-detail drawer (with per-task comments).

```tsx theme={null}
import { ProjectModule } from '@statisfy/digital-workers-react'

// Resolve the onboarding project from the session automatically:
<ProjectModule />

// …or render a specific project:
<ProjectModule projectId="1102" />
```

### `<Comments/>`

A standalone comment thread + composer — project-level, or task-level with a `taskId`. Used inside `<ProjectModule/>`, but also available on its own for custom layouts.

```tsx theme={null}
import { Comments } from '@statisfy/digital-workers-react'

<Comments projectId="1102" />                 {/* project thread */}
<Comments projectId="1102" taskId="t1" />      {/* task thread */}
```

### `useProject(projectId?)`

Headless data hook for custom layouts. Owns the config → project resolve, the milestone/subtask tree, and optimistic task/project writes with rollback.

```tsx theme={null}
import { useProject } from '@statisfy/digital-workers-react'

const { project, milestones, tasksByParent, canEdit, loading, error, updateTask } = useProject()
await updateTask('t1', { status: '10' })
```

### `useComments(projectId, taskId?)`

Headless comments hook. Loads customer-visible comments and posts new ones (optimistically appended).

```tsx theme={null}
import { useComments } from '@statisfy/digital-workers-react'

const { comments, loading, posting, addComment } = useComments('1102', 't1')
await addComment('Thanks — looks good!')
```

### `createProjectClient(config)`

The React-unaware transport, if you want to call the API without hooks. Attaches the bearer token and publishable key on every request.

```ts theme={null}
import { createProjectClient } from '@statisfy/digital-workers-react'

const client = createProjectClient({ baseUrl, getToken, publishableKey })
const { data } = await client.getProject('1102')
await client.addTaskComment('1102', 't1', 'Done!')
```

| Method                                                              | Endpoint                                              |
| ------------------------------------------------------------------- | ----------------------------------------------------- |
| `getConfig(module)`                                                 | `GET /config/{module}`                                |
| `getProject(id)`                                                    | `GET /projects/{id}`                                  |
| `patchProject(id, properties)`                                      | `PATCH /projects/{id}`                                |
| `patchTask(id, taskId, properties)`                                 | `PATCH /projects/{id}/tasks/{taskId}`                 |
| `getProjectComments(id)` / `addProjectComment(id, value)`           | `GET` / `POST /projects/{id}/comments`                |
| `getTaskComments(id, taskId)` / `addTaskComment(id, taskId, value)` | `GET` / `POST /projects/{id}/tasks/{taskId}/comments` |
| `editComment(projectId, commentId, value)`                          | `PATCH /projects/{id}/comments/{commentId}`           |
