# Portals

Client portal templates and the live portal instances created from them.

**Base URL:** `https://api.trustpager.com/functions/v1/api/v1`

## Endpoints

### POST /portals/:portal_id/members

Add a member to a portal by email and (by default) send them the workspace-branded invite. Adding IS inviting: the backend creates or links a company-less external login (broker, client, site staff who NEVER join your company), sends the invite/access email, and writes a log row. The path is chosen automatically from the person's identity state: a brand-new person gets a set-password link; an un-activated account gets a fresh set-password link re-issued; an already-active platform user gets an 'access granted' notice. Set `send_email: false` to add them silently, with the same access and portal chat but no email. Idempotent per email. Returns `state` (new | pending | active) and `email_sent`. Identical to invite_portal_member.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-portal-id-members.md)

### POST /portals/:portal_id/referrer

Connect a referral partner (a contact) to a partner portal as its bound referrer. The referral dashboard and the portal's "Submit a referral now" form both attribute referrals to this contact, so a portal whose referrer is missing cannot accept referrals until this is set. The contact must already exist in the workspace; connecting it also flags it as a referral partner so it appears in the referrer picker. Use this to fix a portal reported as "not fully set up" or to re-point a portal at a different partner.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-portal-id-referrer.md)

### POST /portals

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.

**Scopes:** `portals:write` — [full detail](./portals/post-portals.md)

### POST /portals/templates

Create a reusable portal template (design only). Put branding / welcome / contact / pages / tools / nav into config; instantiate_portal_template later stamps it onto an opportunity.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-templates.md)

### DELETE /portals/:portal_id

Delete a portal. Cascades to its members and shares (the underlying shared records are NOT deleted).

**Scopes:** `portals:delete` — [full detail](./portals/delete-portals-portal-id.md)

### DELETE /portals/templates/:template_id

Delete a portal template. Portals already instantiated from it are unaffected.

**Scopes:** `portals:delete` — [full detail](./portals/delete-portals-templates-template-id.md)

### GET /portals/:portal_id

Fetch a portal by UUID.

**Scopes:** `portals:read` — [full detail](./portals/get-portals-portal-id.md)

### GET /portals/templates/:template_id

Fetch a portal template by UUID (its full design config).

**Scopes:** `portals:read` — [full detail](./portals/get-portals-templates-template-id.md)

### POST /portals/templates/:template_id/instantiate-for-contact

Create a NEW portal from a template anchored to a CONTACT (a referrer) — the partner-portal path. Pins referral.referrerContactId (commission dashboard scopes to them) and optionally documents_opportunity_id (the referrer's own opportunity that supplies the templateContent signing/document/file candidates + per-instance signing variables). The template's templateContent proliferates into the instance automatically. Use this instead of instantiate_portal_template for partner portals.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-templates-template-id-instantiate-for-contact.md)

### POST /portals/templates/:template_id/instantiate

Create a NEW portal from a template, anchored to an opportunity. Copies the template design and sets config.anchor to the opportunity; optionally seeds members. Returns the new portal. Share records into it afterwards with share_to_portal.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-templates-template-id-instantiate.md)

### POST /portals/:portal_id/invite

Invite an external person to a portal by email and send them a workspace-branded invite email. Records them as a company-less member (broker / client / site staff) who NEVER joins your company. Handles all three identity states automatically: a brand-new person gets a set-password link; a person with an un-activated account gets a fresh set-password link re-issued; a person already active on the platform gets an 'access granted' notice and signs in with their existing password. Set `send_email: false` to grant access silently instead. Idempotent per email. Returns `state` (new | pending | active) and `email_sent`.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-portal-id-invite.md)

### GET /portals/:portal_id/members

List the members of a portal.

**Scopes:** `portals:read` — [full detail](./portals/get-portals-portal-id-members.md)

### GET /portals/:portal_id/shares

List the records shared into a portal.

**Scopes:** `portals:read` — [full detail](./portals/get-portals-portal-id-shares.md)

### GET /portals/template-content-candidates

List the workspace resources valid to attach as template content of a given type (so you attach real ids, not guesses). signing/documents → document templates; files → secure files; images → company images; notepads → notepads.

**Scopes:** `portals:read` — [full detail](./portals/get-portals-template-content-candidates.md)

### GET /portals/templates

List reusable portal templates (designs). A template carries branding / welcome / contact / pages / tools / nav order in config, with no anchor, members, or shares.

**Scopes:** `portals:read` — [full detail](./portals/get-portals-templates.md)

### GET /portals

List client/partner portals in the workspace.

**Scopes:** `portals:read` — [full detail](./portals/get-portals.md)

### POST /portals/:portal_id/reconcile-autoshare

Back-fill a live portal: share the anchored opportunity's EXISTING records for every dataSharing type set to enabled+all. New records auto-share via triggers; call this once after enabling a type so content that predates the rule also appears.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-portal-id-reconcile-autoshare.md)

### DELETE /portals/:portal_id/members/:member_id

Remove a member from a portal (revokes their access to it).

**Scopes:** `portals:write` — [full detail](./portals/delete-portals-portal-id-members-member-id.md)

### DELETE /portals/:portal_id/shares/:share_id

Stop sharing a record into a portal (members lose access to it). Does not delete the underlying record.

**Scopes:** `portals:write` — [full detail](./portals/delete-portals-portal-id-shares-share-id.md)

### POST /portals/data-sharing

Safely set ONE opportunity-auto-share rule (Container 1) on a portal instance OR a template — server-side merge, so you never replace the whole config. enabled=false fully protects the type; mode "all" auto-shares all of the anchored opportunity's records of that type, "selected" limits to resource_ids.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-data-sharing.md)

### POST /portals/template-alignment

Turn "keep all instances aligned with this template" on or off for a portal TEMPLATE. When enabled=true, every instance of this template — existing and future — keeps its DESIGN (tabs/pages, page content, branding, welcome, contact) matched to the template: switching it on aligns the existing instances immediately, and every later template design edit propagates to all of them automatically. enabled=false re-freezes them as independent. Each instance's own opportunity anchor, referrer, members, shared records, and data-sharing rules are always preserved.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-template-alignment.md)

### POST /portals/template-content

Safely set ONE template-content type (Container 2) on a portal TEMPLATE — server-side merge. This content proliferates into every instance (signing/documents rendered per instance with the referrer/opportunity variables). Pass the full desired id list for the type (replaces just that type's list). Use list_portal_template_content_candidates to find valid ids.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-template-content.md)

### POST /portals/:portal_id/shares

Share a workspace record into a portal so its members can see it. This is the explicit access grant - nothing appears in a portal until shared. Idempotent. resource_type is one of: document, file, image, invoice, signing_envelope, booking, scheduled_event_type, form_template, form_submission, work_order, work_order_portal, referral, commission_payment, notepad, notepad_folder, transcript, opportunity_notepad. Scheduler (scheduled_event_type) and form_template surface as Tools the member can act on; documents, invoices and signing surface as Details; opportunity_notepad surfaces as a member-editable notepad in the portal.

**Scopes:** `portals:write` — [full detail](./portals/post-portals-portal-id-shares.md)

### PATCH /portals/:portal_id

Update a portal. Every field is optional and independent: whatever you omit is left exactly as stored. Set status to disabled to suspend member access. Passing config REPLACES the whole object (get → merge → patch), but omitting name, type, or status does NOT reset them, so do not echo them back unless you mean to change them.

**Scopes:** `portals:write` — [full detail](./portals/patch-portals-portal-id.md)

### PATCH /portals/templates/:template_id

Update a portal template. Every field is optional and independent: whatever you omit is left exactly as stored. Passing config REPLACES the stored design (get → merge → patch the whole object), but omitting name or type does NOT reset them, so do not echo them back unless you mean to change them.

**Scopes:** `portals:write` — [full detail](./portals/patch-portals-templates-template-id.md)
