Skip to content

POST /api/v1/admin/invites/:invite_id/accept

This page is auto-generated from src/admin/routes.ts. Do not edit by hand — see docs/dev/docs-auto-sync.md.

:warning: Heuristic extraction — not a contract. Response shapes below are mined from c.json(...) literals in the handler via an AST walker. Computed fields, ternaries, and non-literal returns are shown as placeholders. Refer to the linked source file for the authoritative behaviour.

POST /api/v1/admin/invites/:invite_id/accept

Section titled “POST /api/v1/admin/invites/:invite_id/accept”

No requireCapability middleware detected on this route. Surface-level auth (admin token, dashboard session, org API key, anonymous proxy) is enforced by the parent router — see Source below.

Other middleware observed on this route (heuristic):

  • adminBodyLimit

POST /api/v1/admin/invites/:invite_id/accept — close the accept loop. Deletes the pending_invites row + emits the member_accepted audit row.

SECURITY MODEL — the API enforces; the dashboard is UX.

The accept handler is a security device. The admin-token bearer (whether forwarded by the dashboard or used directly by an operator tool) MUST NOT be able to bind an arbitrary email to an invite by passing a fraudulent accepted_by_user_id — that would let anyone with access to this endpoint silently convert an alice@… invite into a bob@… acceptance, corrupting the audit chain and (if a future change makes the accept handler grant state) granting access to the wrong principal.

Defense steps applied at the API layer (NEVER rely on the dashboard):

1. `accepted_by_user_id` MUST carry the `stytch:` prefix. The
admin-token sentinel and other non-Stytch principals are
explicitly rejected — the only legitimate accepter is a
Stytch-authenticated member.
2. Tenant MUST be bound to a Stytch organization (the invite
couldn't legitimately be issued otherwise).
3. Call Stytch `organizations.members.get` with the bound org +
the supplied member id. This proves the member actually exists
in the Stytch org the invite was issued for, AND surfaces the
member's verified email from Stytch's source of truth.
4. Compare the Stytch-returned email (case-insensitive, normalised)
to the invite's email. Mismatch → 403 + structured error log.
The pending_invites row is NOT deleted on mismatch — a legitimate
invitee can still complete the flow.
5. Only after all above pass: delete the invite + emit the
`member_accepted` audit envelope.

The dashboard’s UX-side email check is a fast-feedback layer for users; the API does its own check independently and authoritatively.

{
"error": "Invalid JSON body"
}
{
"error": "Body must be a JSON object"
}
{
"error": "accepted_by_user_id is required"
}
{
"error": "accepted_by_user_id must be a Stytch-authenticated principal (stytch:<member_id>)"
}
{
"error": "accepted_by_user_id is malformed"
}
{
"error": "Invite not found"
}
{
"error": "Invite expired"
}
{
"error": "Tenant has no bound Stytch organization — invite cannot be completed"
}
{
"error": "Stytch client unavailable; cannot verify invitee identity"
}
{
"error": "accepted_by_user_id is not a member of this tenant's Stytch organization"
}
{
"error": "Accepting principal's email does not match the invite email"
}
{
"error": "Tenant not found"
}
{
"error": "Seat cap reached for this tier",
"consumed": <consumedExcludingThis>,
"cap": <cap>
}
{
"invite_id": <member-expr>,
"tenant_id": <member-expr>,
"tenant_slug": <logical-expr>,
"accepted": true
}
  • File: src/admin/routes.ts
  • Line: 2360