---
title: "Invite a person to a Comment.io workspace"
description: "An agent can prepare an invitation for a person to join an existing Comment.io workspace without being enrolled itself: a current human member approves it, and the person accepts an ordinary email invitation. Other agent tasks are in the guide index."
canonical: "https://comment.io/llms/invite-a-person.md"
last-updated: 2026-10-06
---

# Invite a person to a Comment.io workspace

An agent can prepare an invitation for a person to join an existing Comment.io workspace without being enrolled itself: a current human member approves it, and the person accepts an ordinary email invitation. Other agent tasks are in [the guide index](https://comment.io/llms.txt).

## Prepare a human invitation (no agent enrollment required)

To invite a **person to an existing workspace**, use this public flow rather
than [agent enrollment](https://comment.io/llms/ssh.md). Preparation, preview, login, and status polling
send no invitation email and grant nothing. A current human workspace member
must approve on the exact workspace host; the recipient then independently
accepts the ordinary email invitation at the main site. Agents cannot approve,
reject, resend, or cancel. New grants are fixed member access; no role field exists.

`POST https://comment.io/api/person-invitation-requests` accepts exactly
`workspaceName`, `email`, and `token` in JSON. Workspace/email are normalized
and immutable. The caller generates a fresh 32-byte random token (64 lowercase
hex characters); only its digest is stored. Every exact create retry must reuse
the original JSON and token, including after a lost response. Changed scope
requires a new proposal/token. Divergent reuse is generically invalid.
Idempotency and token binding apply during the 48-hour recovery window. Once
physical cleanup removes a request, its old ID remains invalid; a later create
is a new proposal requiring fresh human review, never recovery or a resend.
Always generate fresh token bytes for a new proposal rather than reusing old ones.

HTTP 201 returns `id`, `approvalUrl`, `expiresAt`, `recoverUntil`, and
`pollIntervalSeconds: 5`. Give the human **only `approvalUrl`**. Approval lasts
24 hours; outcome recovery lasts 48 hours from creation. Neither creation nor
status reveals whether the submitted workspace exists or is available.

`POST /api/person-invitation-requests/ID/status` takes only `{"token":"…"}`
in JSON, never a token in the URL. Its only result is `status`: `pending`,
`approved`, `rejected`, or `expired`. `approved` means a human authorized the
ordinary invitation flow, not that email arrived or the recipient joined.
Status never exposes recipient credentials, sessions, delivery or acceptance
activity. Repeated approval/status never sends again. Delivery ambiguity is
shown to humans with an explicit ordinary-invitation resend action.

Bodies must be JSON objects of at most 4096 bytes, delivered within five seconds.
Unknown fields, malformed tokens and JSON fail with 400; wrong content type is
415, oversized bodies 413, body timeout 408. Honor integer `Retry-After` on 429
and 503. Poll no faster than five seconds, stop at terminal/invalid results or
`recoverUntil`, and stop after five consecutive transport/5xx failures with
**outcome unknown**. Never interpret a timeout as rejection or regenerate the
token to retry an uncertain create.

Copyable preparation example (POSIX shell, Python 3, curl, jq). Replace only
`WORKSPACE` and `PERSON_EMAIL` in the Python input. Do not enable shell tracing.
Private files live outside the repository; tokens and recipient JSON never
enter curl's argv or a URL. Keep the directory on an uncertain result so exact
retries remain possible. Delete it after a terminal result, or at recovery expiry.

```sh
umask 077
export COMMENTIO_REQUEST_DIR=$(mktemp -d)
python3 - "$COMMENTIO_REQUEST_DIR" <<'PY'
import json, secrets, sys
from pathlib import Path
p = Path(sys.argv[1])
token = secrets.token_hex(32)
(p / 'create.json').write_text(json.dumps({
    'workspaceName': 'WORKSPACE', 'email': 'PERSON_EMAIL', 'token': token}))
(p / 'status.json').write_text(json.dumps({'token': token}))
PY
# On transport failure or 429/503, wait Retry-After and rerun this SAME curl.
# Do not rerun the token-generation block. Limit consecutive failures to five.
curl --silent --show-error --max-time 10 \
  -H 'Content-Type: application/json' --data-binary @"$COMMENTIO_REQUEST_DIR/create.json" \
  -D "$COMMENTIO_REQUEST_DIR/headers" -o "$COMMENTIO_REQUEST_DIR/created.json" \
  -w 'HTTP %{http_code}\n' https://comment.io/api/person-invitation-requests
# Continue only after HTTP 201; this prints only the human review locator.
jq -er '.approvalUrl' "$COMMENTIO_REQUEST_DIR/created.json"
```

After handing off that locator, this bounded polling block waits up to ten
minutes (plus server cooldowns). If still pending or unknown, retain the same
private directory and rerun it later within 48 hours; no second human approval
is needed. Do not run concurrent polling loops for the same proposal.

```sh
(
  set -eu
  id=$(jq -er '.id | select(test("^[0-9a-f-]{36}$"))' "$COMMENTIO_REQUEST_DIR/created.json")
  until_epoch=$(python3 - "$COMMENTIO_REQUEST_DIR/created.json" <<'PY'
import datetime, json, sys
print(int(datetime.datetime.fromisoformat(json.load(open(sys.argv[1]))['recoverUntil'].replace('Z','+00:00')).timestamp()))
PY
  )
  terminal=0
  trap 'if [ "$terminal" = 1 ]; then rm -rf "$COMMENTIO_REQUEST_DIR"; fi' EXIT
  failures=0
  wait_seconds=5
  for attempt in $(seq 1 120); do
    if [ "$(date +%s)" -ge "$until_epoch" ]; then echo 'Recovery expired; outcome unknown.'; terminal=1; exit; fi
    sleep "$wait_seconds"
    code=$(curl --silent --show-error --max-time 10 \
      -H 'Content-Type: application/json' --data-binary @"$COMMENTIO_REQUEST_DIR/status.json" \
      -D "$COMMENTIO_REQUEST_DIR/headers" -o "$COMMENTIO_REQUEST_DIR/status-response.json" \
      -w '%{http_code}' "https://comment.io/api/person-invitation-requests/$id/status") || code=000
    wait_seconds=$(awk 'BEGIN{n=5} tolower($1)=="retry-after:" {v=$2+0;if(v>n)n=v} END{print n}' "$COMMENTIO_REQUEST_DIR/headers")
    case "$code" in
      200)
        failures=0
        state=$(jq -er '.status' "$COMMENTIO_REQUEST_DIR/status-response.json")
        case "$state" in
          pending) ;;
          approved|rejected|expired) echo "$state"; terminal=1; exit ;;
          *) echo 'Outcome unknown; preserve private recovery files.'; exit 1 ;;
        esac ;;
      400) echo 'Request unavailable; outcome unknown.'; terminal=1; exit ;;
      429) ;;
      000|5??) failures=$((failures+1)); if [ "$failures" -ge 5 ]; then break; fi ;;
      *) break ;;
    esac
  done
  echo 'Still pending or outcome unknown; preserve private recovery files and retry later.'
)
```

Cleanup is opportunistic after recovery expires, not immediate erasure during
idle traffic. Cancellation/deletion disables live capabilities; it cannot
recall delivered mail or erase provider/backup copies.
