Client Portal API
REST endpoints for the Client Portal module — portal projects, milestones, deliverables, messages, reports.
There are two API surfaces for portal data:
/api/portal/*— used by the tenant team (authenticated session)./api/client/*— used by the client guest (magic-link session).
The split prevents accidentally exposing internal-only fields to the client view.
Team endpoints (/api/portal/*)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/portal/projects | List portal projects. |
| POST | /api/portal/projects | Create. |
| GET/PATCH/DELETE | /api/portal/projects/{id} | CRUD. |
| GET | /api/portal/projects/{id}/messages | List messages including is_internal=true. |
| POST | /api/portal/projects/{id}/messages | Post a message (may set is_internal). |
| GET | /api/portal/projects/{id}/deliverables | List. |
| POST | /api/portal/projects/{id}/deliverables | Upload + create. |
| GET | /api/portal/projects/{id}/milestones | List. |
| POST | /api/portal/projects/{id}/milestones | Create. |
| GET | /api/portal/projects/{id}/reports | List AI-generated reports. |
| POST | /api/portal/file-requests | Issue a file-upload request to the client. |
| POST | /api/portal/invite | Send a magic-link invite to a client. |
Magic-link session (shared)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/portal/verify | Exchange magic-link token for a portal_session cookie. |
| GET | /api/portal/session | Current session info. |
Client guest endpoints (/api/client/*)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/client/projects/{id} | Project detail (excludes internal-only fields). |
| GET | /api/client/projects/{id}/messages | Messages (filtered to is_internal=false). |
| POST | /api/client/projects/{id}/messages | Post a client message. Triggers internal notifications + emits portal.message.received. |
| GET | /api/client/dashboard | Aggregated portal overview for the client landing page. |
Key entity shapes
Portal project
{
"id": "uuid",
"name": "string",
"project_id": "uuid?",
"client_id": "uuid?",
"status": "active | completed | on_hold",
"start_date": "iso-8601?",
"end_date": "iso-8601?",
"progress": 0,
"metadata": {}
}
Portal message
{
"id": "uuid",
"portal_project_id": "uuid",
"sender_id": "uuid?",
"sender_email": "string?",
"content": "string",
"attachments": [],
"is_internal": false,
"created_at": "iso-8601"
}
sender_id is set for team members; sender_email is set for
client guests (no Supabase user row).
Permissions
Seeded slugs: portal.messages.read|create. Projects /
deliverables / file-requests / invites all gate on
portal.messages.* overlays plus tenant-admin checks — there are
no separate seeded portal.projects.*, portal.file_requests.*,
or portal.invite slugs.
Client-guest side: gated by the magic-link session cookie — no permission system on the client surface (every authenticated client sees the portal_project they were invited to).
Events
portal.message.sent(team-originated)portal.message.received(client-originated)portal.file.uploaded
Deliverable + project lifecycle events are tracked in the in-app activity feed but not emitted to the platform event bus today.
OpenAPI
Full schema at /api/openapi.json.