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

# Client Portals

> Create portals from templates, control what a client sees per phase, and follow what they do

A client portal (a *space*) is a page you share with one client: documents, forms, file uploads, action plans and a scheduler, behind an email gate. Your system can drive portals end to end: create one per client from a template you maintain in the dashboard, unlock sections as your process advances, and receive webhooks when the client fills a form, uploads a file or checks off a step.

## Key Concepts

| Concept        | Description                                                                                               |
| -------------- | --------------------------------------------------------------------------------------------------------- |
| **Space**      | One portal, for one client. Has sections, a status (`draft`, `published`) and share links                 |
| **Template**   | A space flagged as a template. Build it once in the dashboard; instantiate it per client through the API  |
| **Section**    | A titled group of blocks with its own visibility                                                          |
| **Block**      | Content inside a section: `form`, `upload`, `task` (action plan), `content`, `text`, `scheduler`, …       |
| **Visibility** | `visible`, `locked` (title shown, content withheld) or `hidden`, on sections and blocks                   |
| **Viewer**     | The person who opened the portal, identified by email; verified through an invite link or a one-time code |

## Authentication

Use a [personal access token](/developer-docs/personal-access-tokens) with the **Portals** capability (`spaces`), or a full-access token. A token limited to other capabilities receives `403 This token does not carry the 'spaces' capability.`

```bash theme={null}
curl https://api.meetergo.com/space/templates \
  -H "Authorization: Bearer YOUR_TOKEN"
```

## Create a portal from a template

Build the template in the dashboard (Portals → ⋯ → *Save as template*), then list and instantiate it:

```bash theme={null}
# Your templates
curl https://api.meetergo.com/space/templates \
  -H "Authorization: Bearer YOUR_TOKEN"

# A working portal for one client
curl -X POST https://api.meetergo.com/space/from-template \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceSpaceId": "TEMPLATE_ID",
    "customerCompanyName": "Example GmbH",
    "brandOverrides": { "primaryColor": "#0f766e" }
  }'
```

The response is the complete portal: every section and block with its id, so you can address them later. The copy is made in one transaction, and for a new client:

* the previous client's name in titles, descriptions and steps is replaced by `customerCompanyName`
* the template's client-specific branding (logo, colours, backdrop) is dropped and `brandOverrides` applied
* action-plan progress starts at zero

| Field                                       | Description                                                                |
| ------------------------------------------- | -------------------------------------------------------------------------- |
| `sourceSpaceId`                             | A template, or any of your spaces                                          |
| `name`                                      | Optional. Defaults to the template name plus the client name               |
| `customerCompanyId` / `customerCompanyName` | Link an existing client company, or create one by name                     |
| `brandOverrides`                            | The client's look: `logo`, `primaryColor`, `secondaryColor`, `backdropUrl` |
| `dealId`                                    | Optional CRM deal to attach the portal to                                  |

Related: `POST /space/{spaceId}/duplicate` (same client, "(Copy)" suffix), `POST /space/{spaceId}/save-as-template`, `POST /space/{spaceId}/section-from-template` (`{ "templateId", "index" }`) and `POST /space-section-template/from-section` for reusable sections.

## Control what the client sees

Portals are phase-driven: keep later requirements hidden or locked until the client gets there. Both levels take the same body.

```bash theme={null}
# Unlock a whole section
curl -X PATCH https://api.meetergo.com/space/SPACE_ID/section/SECTION_ID \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "visibility": "visible" }'

# Lock a single block (the client sees its title, not its content)
curl -X PATCH https://api.meetergo.com/space/SPACE_ID/section/SECTION_ID/block/BLOCK_ID \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "visibility": "locked" }'
```

Nothing has to be created or deleted per phase: a template carries every block, and your system flips visibility.

## Invite the client

```bash theme={null}
# Publish, then create a share link
curl -X POST https://api.meetergo.com/space/SPACE_ID/publish \
  -H "Authorization: Bearer YOUR_TOKEN"

curl -X POST https://api.meetergo.com/space/SPACE_ID/link \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Client link", "requireEmail": true, "requireEmailOtp": true, "allowUploads": true }'

# Invite a person. sendEmail: true lets meetergo send the invitation;
# false returns an inviteToken so you can send your own link.
curl -X POST https://api.meetergo.com/space/SPACE_ID/people \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "anna@example.com", "name": "Anna Example", "sendEmail": false }'
```

The personal link is `https://cal.meetergo.com/spaces/{slug}?invite={inviteToken}` (your own domain instead of `cal.meetergo.com` when a custom domain is configured). Opening it verifies the person's email without a code; a link with `requireEmailOtp` asks anyone else for a one-time code.

## Read the state of a portal

`GET /space/{spaceId}` returns sections, blocks and, on `task` blocks, every step:

```json theme={null}
{
  "id": "step-2",
  "description": "Upload your CV",
  "done": true,
  "dueDate": "2026-09-10",
  "phase": "Phase 1",
  "assignee": { "name": "Anna Example", "side": "them" },
  "completedAt": "2026-09-02T14:05:12.000Z",
  "updatedAt": "2026-09-02T14:05:12.000Z"
}
```

`assignee.side` is `them` for the client and `us` for your team. `completedAt` and `updatedAt` are set by the server.

**Forms.** `GET /space/{spaceId}/form-status` lists every form block in the portal with the submissions made *inside this portal*: whether the client handed it in, when, by whom, and the answers.

```json theme={null}
{
  "spaceId": "…",
  "blocks": [
    {
      "blockId": "…",
      "formId": "…",
      "formName": "CV",
      "visibility": "visible",
      "submitted": true,
      "latestSubmissionAt": "2026-09-02T14:05:12.000Z",
      "submissions": [
        { "id": "…", "submittedAt": "…", "viewerEmail": "anna@example.com", "data": { "email": "…", "phone": "…" } }
      ]
    }
  ]
}
```

**Files.** `GET /space/{spaceId}/uploads` lists what the client uploaded to the upload zone (file name, type, size, who, when). `GET /space/{spaceId}/uploads/{uploadId}/download` returns a short-lived download URL. Files attached to a form answer arrive with that form's submission instead.

## Follow what the client does

Subscribe to the [portal webhooks](/developer-docs/webhooks/events#client-portal-events): `space_viewed`, `space_invite_accepted`, `space_task_completed`, `space_task_reopened`, `space_file_uploaded`. A form filled inside a portal arrives as `form_submission` with a `space` object naming the portal, block and viewer.

Every portal event carries `spaceId`, the acting `viewer` and `occurredAt`, so it can be matched to your own records without a follow-up request.

## Errors

| Status | Meaning                                                                                             |
| ------ | --------------------------------------------------------------------------------------------------- |
| `400`  | Validation, e.g. `sourceSpaceId must be a UUID`, `visibility must be a valid enum value`            |
| `403`  | Token lacks the `spaces` capability, or the user may not manage this space                          |
| `404`  | Space, section, block or template not found in your company. The message names what to list instead |
