---
title: "Invite a colleague (preview, then confirm)"
method: POST
path: "/api/v1/workspaces/{workspaceId}/members"
tags: ["Team"]
---

# Invite a colleague (preview, then confirm)

`POST /api/v1/workspaces/{workspaceId}/members`

## Request

- Base URL: `https://www.poster.ly`
- URL: `https://www.poster.ly/api/v1/workspaces/{workspaceId}/members`
- Auth: HTTP bearer

Owner or admin. Always creates a pending invite and emails it, even when the address already has a posterly account (it joins at their next sign-in if they do not click the link). The email subject names the inviter (their name with their email address, or just their email address) and the workspace. Admin seats are dashboard-only over the API. The invite counts toward the plan seat limit while pending. The workspace owner is emailed about every API team change, naming the API key.
Two steps. Without `confirm: true` the call returns 200 with `requires_confirmation`, a plain-language `preview` and a short-lived `preview_id`, and changes nothing. Show the preview to the user; after they approve, repeat the call with `confirm: true` and the `preview_id`. If anything changed in between, the confirm answers 409 `preview_stale` and changes nothing (a safeguard for the user, not a security control: every rule is checked again on confirm).
Access: an API key that a person granted "Manage team" in /dashboard/api (members:read and members:write). Browser sessions are refused with `api_key_required`; the scopes can never be requested through POST /api/v1/api-keys. ChatGPT, Claude and other OAuth connections hold both scopes implicitly (when their grant includes accounts:write) but may call this endpoint only while the person is an owner or admin of the workspace; otherwise 403 `team_management_requires_admin`. A key bound to another workspace, or a caller who is not a member, gets 404; a caller whose seat was removed gets 403 `workspace_access_revoked`.
Repeats: the same email, role and brands answers 200 `already_invited` and sends nothing unless `resend: true`. A current member answers 409 `already_member` (use PATCH). Send an `Idempotency-Key` header on the confirm call to make retries safe.
Limits (previews count): 10 invite requests per workspace and 30 per user per 24 hours, and 3 emails per address per workspace per 24 hours. Confirmed invites are processed one at a time per workspace so two parallel calls cannot both take the last seat.
Error codes: `api_key_required`, `insufficient_scope`, `insufficient_role`, `team_management_requires_admin` (403, OAuth connection below admin), `admin_role_dashboard_only`, `plan_required`, `subscription_required` (402), `seat_limit_reached` (403, with seats_used and seat_limit), `validation_error`, `cannot_invite_self`, `invalid_brand_ids`, `preview_required`, `preview_stale` (409), `already_member` (409), `invite_in_progress` (409), `idempotency_key_reused` (409), `rate_limited` / `recipient_rate_limited` (429), `temporarily_unavailable` (503).

## Path parameters

- `workspaceId` string, uuid, required

## Headers

- `Idempotency-Key` string

## Request body

- InviteWorkspaceMemberRequest
  - `email` string, email, required
  - `role` 'publisher' | 'editor' | 'viewer', required: Admin seats are dashboard-only: `admin` answers 403 admin_role_dashboard_only.
  - `brand_ids` string[]: Limit the seat to these brands. Empty or omitted means every brand.
  - `resend` boolean: Email an unchanged pending invite again (capped at 3 emails per address per workspace per 24 hours).
  - `confirm` boolean: Omit (or false) for a preview. true, with preview_id, to act.
  - `preview_id` string: From the preview response. Required with confirm: true.

## Response `200`

Preview (nothing changed), or already_invited (nothing sent).

- union
  - WorkspaceMemberPreviewResponse: Returned when confirm is not true. Nothing was written and no email was sent.
    - `requires_confirmation` true
    - `preview` object
      - `action` 'invite' | 'reinvite' | 'resend_invite' | 'add_member' | 'update_member' | 'update_invite' | 'remove_member' | 'revoke_invite' | 'set_api_access'
      - `summary` string: Plain-language description to show the user before confirming.
      - `email` string, nullable
      - `role` string, nullable
      - `previous_role` string, nullable
      - `brand_ids` string[], nullable
      - `previous_brand_ids` string[], nullable
      - `sends_email` boolean
      - `counts_toward_seats` boolean
      - `seats_used` integer, nullable
      - `seat_limit` integer, nullable: -1 means no limit.
      - `api_access` boolean: set_api_access only: the new setting.
      - `previous_api_access` boolean, nullable: set_api_access only: the setting before this change.
    - `preview_id` string: Short-lived (15 minutes). Pass back with confirm: true.
    - `preview_expires_at` string, date-time
    - `next_step` string
  - WorkspaceMemberChangeResponse
    - `status` 'invited' | 'invite_updated' | 'invite_resent' | 'already_invited' | 'added' | 'updated' | 'removed' | 'revoked'
    - `invite` WorkspaceInvite: A pending invite. Never includes the invite link or token.
      - `id` string, uuid: Invite ID (use in /invites/{inviteId}).
      - `email` string
      - `role` 'admin' | 'publisher' | 'editor' | 'viewer'
      - `status` 'pending' | 'revoked' | 'accepted' | 'expired'
      - `brand_ids` string[]
      - `expires_at` string, date-time, nullable
      - `created_at` string, date-time, nullable
    - `member` WorkspaceMember
      - `id` string, uuid: Member ID (use in /members/{memberId}).
      - `user_id` string, nullable
      - `email` string, nullable
      - `name` string, nullable
      - `role` 'owner' | 'admin' | 'publisher' | 'editor' | 'viewer'
      - `status` 'active' | 'invited' | 'removed'
      - `brand_ids` string[]: Brands this seat is limited to. Empty means every brand. Missing when a brand-limited caller cannot see this seat assignment.
      - `api_access` boolean: Whether this teammate can use the workspace API plan from their own API keys and AI apps. Off by default; always true for the owner, whose own add-on decides.
      - `api_usage` object: Owners and admins only: live API keys and AI app connections that can act in this workspace for this member.
        - `key_count` integer
        - `last_used_at` string, date-time, nullable
        - `connected_apps` string[]
      - `joined_at` string, date-time, nullable
      - `created_at` string, date-time, nullable
    - `email_sent` boolean
    - `owner_notified` boolean: True when the owner alert email about this change was sent.
    - `message` string

## Other responses

- `201`: Invite created (or updated) and emailed.
- `400`: Validation error
- `401`: Invalid or missing API key
- `402`: Request is authenticated but not allowed
- `403`: Request is authenticated but not allowed
- `404`: Resource not found
- `409`: Resource is not in a usable state for this request
- `429`: Too many requests
- `503`: Temporarily unavailable. Retry, or fall back to the documented alternative.

---

[API](https://skmtc.dev/poster/apis/posterly-api.md) · [All operations](https://skmtc.dev/poster/apis/posterly-api/llms.txt) · [OpenAPI document](https://skmtc.dev/poster/apis/posterly-api/revisions/60be02a61f95?raw)
