---
title: "Preview Invite"
method: GET
path: "/api/team/invites/{token}/preview"
tags: ["team"]
---

# Preview Invite

`GET /api/team/invites/{token}/preview`

## Request

- Base URL: `https://api.heymetra.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.heymetra.com/api/team/invites/{token}/preview`
- Auth: none declared

Public: what this invitation is, and whether it can still be used.

Rate limited per IP because it is the only unauthenticated endpoint here
that reads a secret from the URL. The tokens are 32 random bytes, so
guessing one is not a realistic attack — but an endpoint that answers
unlimited guesses is worth throttling on principle rather than on
arithmetic, and the same limit stops it being used to hammer the database.

## Path parameters

- `token` string, required

## Response `200`

Successful Response

- InvitePreviewOut: What the invite screen may say before anybody has signed in. ``state`` is the whole point of this shape. It used to be 404 for every unusable invitation, which left the one person who most needs an answer — somebody holding a link that no longer works — staring at a sign-up form with no idea why it would not help them. The owner's ruling (2026-09-14): *"workspace daveti expired olduysa kullanici bunu login olmadan gormeli."* The workspace name and the invited address are only ever filled in for an invitation that really exists; an unknown token is answered with the state alone, so this cannot be turned into a way to read a workspace's name or a colleague's address by guessing.
  - `state` 'valid' | 'expired' | 'revoked' | 'accepted' | 'unknown', required
  - `tenant_name` string, nullable
  - `role` 'owner' | 'member': Who somebody is in one workspace. Two, by the owner's decision of 2026-09-18, and the argument is the one he made: four roles that collapse to two predicates are not four roles. OWNER and ADMIN differed in two lines in the whole codebase; EDITOR and VIEWER in one. Worse, four roles plus per-connection grants meant the same fact could be stated twice and disagree — a role saying VIEWER while a grant said writer, with the winner decided across sixteen separate gates. One axis cannot express the contradiction. ``OWNER`` does everything: billing, team, connecting accounts, approving changes, and reading and writing on every connection. ``MEMBER`` does exactly what it was given, per connection, in ``connection_grants.level``. A member with no row anywhere can sign in and see that nothing has been shared with them yet, and that is all. Persisted as a plain string: VARCHAR(32), no CHECK constraint, no native Postgres type, so this is a code change and not a migration.
  - `email` string, nullable
  - `has_account` boolean, nullable

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/heymetra/apis/heymetra-api.md) · [All operations](https://skmtc.dev/heymetra/apis/heymetra-api/llms.txt) · [OpenAPI document](https://skmtc.dev/heymetra/apis/heymetra-api/revisions/877b0f87d6d0?raw)
