# POST /portals

**Resource:** [Portals](./portals.md)  
**MCP tool:** `create_portal_instance`  
**Scopes:** `portals:write`  
**Write operation:** yes

Create a portal INSTANCE (a live portal). PREFER instantiate_portal_template to create a portal for an opportunity — it stamps a template onto the opportunity, links it, and auto-shares the right records. Use this lower-level call only when you already hold the template id and anchor. config.templateId is REQUIRED: every instance must derive from a Portal Template (the reusable design, in portal_templates; create one with create_portal_template). A portal is the unit of access: members see only records shared into it. Pass config.anchor to link it to the CRM opportunity, plus optional branding / welcome / contact overlay.

## Parameters

| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| `name` | body | string | yes | Display name, e.g. the opportunity / client name. |
| `type` | body | string | no | partner \| client \| doc_collection \| stakeholder. Defaults to client. client = a private login for a client to track their project. partner = a referrer/broker portal (adds Referrals & commissions). stakeholder = an external contractor/stakeholder portal (pairs with config.stakeholder.enabled). doc_collection = a document-collection portal. |
| `config` | body | object | no | Portal config (jsonb). templateId = <portal_templates uuid> is REQUIRED (every instance derives from a template; get ids from list_portal_templates). Prefer instantiate_portal_template (opportunity) or instantiate_partner_portal (contact/referrer) over building an instance by hand. Portal design config (jsonb). REPLACE-not-merge: get the current config, merge your change, PATCH the whole object back — a partial config wipes the omitted keys. Keys: `branding` {logoUrl,primaryColor,secondaryColor,tagline,icon}; `welcome` {headline,body}; `contact` {name,role,email,phone}; `description` (invite-email blurb); `menus` [{id,label,icon,content?,sidebar:[{type,ref?,slug?,label?,icon?,memberCanEdit?}]}] — the top-nav tabs + sidebar entries (entry types: notepad, notepad_folder, scheduler, form, work_orders, signing, documents, invoices, files, referrals, booking, notepads, transcripts, chat); `dataSharing` {<content_type>: {enabled, mode:"all"\|"selected", resourceIds?, statusIds?, memberCanEdit?, memberEditableFields?}} — Container 1, auto-share of the ANCHORED OPPORTUNITY's content (content types: work_orders, transcripts, invoices, signing, documents, files, bookings, images, notepads). enabled=false fully protects a type; mode "selected" limits to resourceIds; memberEditableFields (work_orders only) is the allowlist of field SLUGS a member may write — omit it for the original "every field editable" behaviour, or pass [] for status-only so a contractor can accept/decline a job offer without being able to retype the offer amount; memberSelectableStatuses (work_orders only) is the same idea for status — the allowlist of status IDS a member may move a job INTO, omitted meaning every workspace status is offered; requiredFieldsByStatus (work_orders only) {"<status id>":["slug"]} gates a transition until those fields are filled ("_attachments" = at least one photo), so a subcontractor cannot mark a job finished before the work is confirmed; `templateContent` {signing?:[document_template_id], documents?:[document_template_id], files?:[secure_file_id], images?:[company_file_id], notepads?:[notepad_id]} — Container 2, TEMPLATE content proliferated into EVERY instance (signing/documents are rendered per instance with the referrer/opportunity variables). Set this on a TEMPLATE; a create/edit trigger proliferates it. `referral` {enabled,commissions,referrerContactId} — PARTNER portals only (commission dashboard); referrerContactId is pinned at instantiation. `stakeholder` {enabled,contractorContactId}: STAKEHOLDER portals only (type "stakeholder"), the external contractor/subcontractor design. enabled=true adds the "My Jobs" (contractor_jobs) + member-editable work_orders tabs; contractorContactId is pinned at instantiation and scopes the jobs dashboard to that contractor across every opportunity they service. `defaultMembers` [{email,name?}] (TEMPLATE only): the internal team seeded as members onto EVERY portal instantiated from this template, so they get in-portal access without an invite email. `documentsOpportunityId` — PARTNER portals only: the referrer's own opportunity that scopes templateContent signing/document/file candidates + variables. `anchor` {type:"opportunity"\|"contact", id} — set by the instantiate endpoints, not by hand. `templateId` — the source template (set at instantiation). `keepInstancesAligned` (TEMPLATE only, boolean, default false) — when true, every instance of this template (existing AND future) keeps its DESIGN (menus, home, branding, welcome, contact, nav, description) matched to the template: any template design edit propagates to all instances automatically, and switching it on aligns the existing instances immediately. Each instance's own opportunity anchor, referrer, members, shared records, and data-sharing rules are always preserved. Set it with set_portal_template_alignment. `lastSyncedAt` (instance only) — stamped by the alignment job when an instance was last matched to its template. |
| `require_approval` | body | boolean | no | Optional. Set true to route this write into the approval queue for human review instead of executing it immediately (returns 202 + an approval_id). Works even when your key/token has permission to execute directly. |

## Request example

```bash
curl -X POST   "https://api.trustpager.com/functions/v1/api/v1/portals"   -H "Authorization: Bearer YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{"name":"...","type":"..."}'
```

---
Base URL: `https://api.trustpager.com/functions/v1/api/v1` — Auth: `Authorization: Bearer YOUR_API_KEY`