⌘K
Endpoints for managing teams, members, invites, billing, and Stripe-portal sessions. Authenticated with a NextAuth session JWT.
These endpoints back the team-management UI in the dashboard. They authenticate with a NextAuth session JWT, not an API key — they're intended for the web app and for tools that drive a logged-in user session (e.g. Playwright integration tests against your own account).
curl https://api.mcpsafe.io/team \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."The token is the NextAuth session JWT issued at sign-in (HS256, signed with the frontend's AUTH_SECRET). API keys are not currently honoured on these routes; if you have a use case for programmatic team management via API key, open an issue.
| Method | Path | Description |
|---|---|---|
| GET | /team | Read the caller's team (members, pending invites, plan, seat counts) |
| POST | /team | Create a team (caller becomes owner) |
| PATCH | /team | Rename or transfer ownership (owner only) |
| POST | /team/invite | Invite a member by email |
| GET | /team/invite-preview | Read the invite as the recipient (called from the accept link) |
| POST | /team/invite/accept | Accept an invite (must match the JWT email) |
| DELETE | /team/invites/{email} | Revoke a pending invite (owner / admin) |
| DELETE | /team/members/{user_id} | Remove a member; passing the caller's own user_id is "leave team" |
| GET | /team/usage | Per-member scan usage for the current calendar month (owner only) |
curl -X POST https://api.mcpsafe.io/team \
-H "Authorization: Bearer ${SESSION_JWT}" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Security"}'curl -X POST https://api.mcpsafe.io/team/invite \
-H "Authorization: Bearer ${SESSION_JWT}" \
-H "Content-Type: application/json" \
-d '{"email": "alice@acme.com", "role": "member"}'role is member or admin. Invites are email-bound — the accepting user's JWT email must match the invite address (case-insensitive). Mismatched accepts return 403 EMAIL_MISMATCH so a forwarded invite link can't be redeemed by a different account.
Invite creation is rate-limited per team: 20 / hour and 100 / day across all owners and admins.
curl -X POST https://api.mcpsafe.io/team/invite/accept \
-H "Authorization: Bearer ${SESSION_JWT}" \
-H "Content-Type: application/json" \
-d '{"invite_token": "tok_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}'# Pending invite — keyed by email (TEAM-19)
curl -X DELETE "https://api.mcpsafe.io/team/invites/alice@acme.com" \
-H "Authorization: Bearer ${SESSION_JWT}"
# Existing member — owner-only; pass own user_id to leave the team
curl -X DELETE "https://api.mcpsafe.io/team/members/usr_xxxxxxxxxxxxxxxx" \
-H "Authorization: Bearer ${SESSION_JWT}"The owner cannot be removed by anyone (including themselves); transfer ownership via PATCH /team first.
curl -X PATCH https://api.mcpsafe.io/team \
-H "Authorization: Bearer ${SESSION_JWT}" \
-H "Content-Type: application/json" \
-d '{"new_owner_user_id": "usr_xxxxxxxxxxxxxxxx"}'The new owner must already be a team member. Self-transfer (passing your own user_id) returns 400 INVALID_FIELD.
Members of a Team or Business plan see all private scans submitted by other members of the same org. This is intentional — it prevents owners from inviting strangers to anonymously consume the team's private-scan quota. Implementation:
user_id (who ran the scan) and org_id (whose plan paid).GET /user/scans and the SSE progress stream resolve the caller's org_id and route to the org-shared GSI.402 SUBSCRIPTION_REQUIRED.curl https://api.mcpsafe.io/team/usage \
-H "Authorization: Bearer ${SESSION_JWT}"Owner-only. Returns the current calendar month's scan totals broken down per member:
{
"period_start": "2026-05-01T00:00:00Z",
"period_end": "2026-05-31T23:59:59Z",
"org": { "org_id": "org_xxx", "name": "Acme Security", "tier": "business" },
"totals": { "scans_used": 412, "members_count": 8 },
"members": [
{ "user_id": "usr_…", "email": "alice@acme.com", "role": "owner", "scans_used": 187, "last_scan_at": "2026-05-06T11:21Z" },
{ "user_id": "usr_…", "email": "bob@acme.com", "role": "admin", "scans_used": 93, "last_scan_at": "2026-05-05T14:02Z" }
]
}| Method | Path | Description |
|---|---|---|
| POST | /checkout/session | Start a Stripe Checkout session for a paid plan |
| POST | /billing/portal | Open a Stripe Customer Portal session (manage card, cancel) |
| POST | /billing/upgrade | Switch plan immediately with proration |
| POST | /billing/cancel | Cancel at period end |
| POST | /billing/reactivate | Undo a pending cancellation |
| GET | /billing/invoices | List past invoices |
| GET | /subscription | Read current plan, renewal date, and entitlement features |
| GET | /me | Read the caller's profile (email, user_id, tier, org_id if any) |
| PUT | /me | Update caller's profile (display name, notification settings) |
curl -X POST https://api.mcpsafe.io/checkout/session \
-H "Authorization: Bearer ${SESSION_JWT}" \
-H "Content-Type: application/json" \
-d '{"tier": "team", "billing_cycle": "annual"}'tier is dev / team / business. billing_cycle is monthly or annual. Optional promo_code and referral_code fields work like the dashboard form. Response: { checkout_url: "https://checkout.stripe.com/…" }.
curl -X POST https://api.mcpsafe.io/billing/portal \
-H "Authorization: Bearer ${SESSION_JWT}"Returns a single-use Stripe Customer Portal URL. From there the user can update payment method, download invoices, cancel, or change plan.
| Code | HTTP | Meaning |
|---|---|---|
EMAIL_MISMATCH | 403 | Accepting user's JWT email doesn't match the invite |
ALREADY_IN_TEAM | 409 | Email is already a member |
INVITE_ALREADY_PENDING | 409 | Email already has an open invite |
CANNOT_REMOVE_OWNER | 403 | Removal target is the team owner |
OWNER_CANNOT_LEAVE | 409 | Owner attempted to remove themselves; transfer first |
NOT_A_MEMBER | 404 | Removal target is not in the team |
INVALID_FIELD | 400 | Self-transfer of ownership, or other body validation failure |
RETRY | 503 | Concurrent write conflict; retry from a fresh GET /team |