MCP OAuth (connector authorization)
The VIVIN API is an OAuth 2.1 authorization server for MCP clients: a landlord signs in with VIVIN and approves the connection, and the client gets a short-lived token for the VIVIN MCP server.
Before you start
| Item | Value |
|---|---|
| Who it is for | Developers who build or debug an MCP client. To connect Claude or ChatGPT, follow Connect Claude, ChatGPT or other AI tools: the client runs this flow for you |
| Base URL | https://api.vivin.app for every endpoint on this page |
| MCP server URL | Shown in Settings → AI / MCP → MCP connection. Users who do not manage AI settings see it in the Connect your AI client card on the same tab |
| Authentication | The /.well-known/* and /oauth/* routes are public: no Authorization header |
| Not the same as | Partner tokens (Authentication) and the app's own sign-in (Management session). A client that cannot do OAuth sends an API key as a Bearer token instead; an Admin or Super Admin generates it in Settings → AI / MCP → API keys |
How the flow works
- The client calls the MCP server URL without a token. The MCP server answers
401withWWW-Authenticate: Bearer resource_metadata="<MCP server>/.well-known/oauth-protected-resource". - That protected-resource metadata names the VIVIN API as the authorization server. The client reads discovery there.
- The client registers and gets a
client_id. - The client opens
GET /oauth/authorizein the landlord's browser with a PKCE challenge. - The landlord clicks Authorize on the consent page. The API redirects to the client's
redirect_uriwith acode. - The client exchanges the code at
POST /oauth/tokenfor an access token and a refresh token. - The client calls the MCP server with
Authorization: Bearer <access_token>and refreshes the token before it expires.
Endpoints
| Method | Path | What it does | Per IP address and minute |
|---|---|---|---|
GET | /.well-known/oauth-authorization-server | Authorization server metadata (RFC 8414) | 120 |
GET | /.well-known/openid-configuration | The same metadata | 120 |
POST | /oauth/register | Dynamic client registration (RFC 7591) | 20 |
GET | /oauth/authorize | The consent page | 60 |
POST | /oauth/authorize | The landlord's decision on the consent page | 10 |
POST | /oauth/token | Authorization-code and refresh-token grants | 60 |
Discovery
GET /.well-known/oauth-authorization-server and GET /.well-known/openid-configuration return the same metadata, with Access-Control-Allow-Origin: * and Cache-Control: public, max-age=3600.
{
"issuer": "https://api.vivin.app",
"authorization_endpoint": "https://api.vivin.app/oauth/authorize",
"token_endpoint": "https://api.vivin.app/oauth/token",
"registration_endpoint": "https://api.vivin.app/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
"scopes_supported": [
"bookings",
"listings",
"properties",
"tenants",
"maintenance",
"payments",
"bills",
"owners",
"analytics",
"channels",
"communication",
"support",
"scheduledReports",
"team"
]
}
- Read the endpoint URLs from this document. Do not hard-code them.
- The protected-resource metadata (RFC 9728) is served by the MCP server, not by the API.
GET /.well-known/oauth-protected-resourceon the API returns404, like any other unknown/.well-known/*path.
Register a client
POST /oauth/register registers a client. Send JSON.
Request fields
| Name | Type | Required | Description |
|---|---|---|---|
redirect_uris | array | Yes | At least one URI, each allowed by the redirect URI policy |
client_name | string | No | Shown on the consent page, as in "Authorize Claude". Cut to 255 characters. Without a name the page says "An MCP connector" |
grant_types | array | No | authorization_code and/or refresh_token. Other values are ignored; with none (or only unsupported ones), both are used |
token_endpoint_auth_method | string | No | none (default: a public client that uses PKCE) or client_secret_post |
scope | string | No | Stored and echoed back. It does not limit access |
Example request
POST /oauth/register HTTP/1.1
Host: api.vivin.app
Content-Type: application/json
{
"client_name": "Claude",
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none"
}
Response
201 Created:
{
"client_id": "mcp-…",
"client_id_issued_at": 1790000000,
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none",
"client_name": "Claude"
}
A client_secret_post client also receives client_secret. It is returned only in this response, so store it.
Errors
| Status | error | error_description | What to do |
|---|---|---|---|
400 | invalid_redirect_uri | redirect_uris must be a non-empty array | Send at least one redirect URI |
400 | invalid_redirect_uri | redirect_uri is not an allowed MCP client callback: <uri> | Use an allowed URI, or an API key |
400 | invalid_client_metadata | grant_types must be an array | Send an array |
400 | invalid_client_metadata | Unsupported token_endpoint_auth_method: <value> | Use none or client_secret_post |
Redirect URI policy
Registration accepts only these redirect URIs:
| Client | Accepted redirect_uri |
|---|---|
| Claude | https://claude.ai/api/mcp/auth_callback |
| ChatGPT | https://chatgpt.com/connector_platform_oauth_redirect, or any path under https://chatgpt.com/connector/oauth/ |
| Desktop and native clients (RFC 8252) | http://localhost or http://127.0.0.1, any port and any path |
Everything else is rejected, including other HTTPS domains, plain http to anything but loopback, and other schemes. A web client on another domain cannot register, so it should use an API key. On authorize and token, the redirect_uri must exactly match one the client registered; loopback URIs match on any port.
Authorize
Open this URL in the landlord's browser, on one line, with every value URL-encoded:
https://api.vivin.app/oauth/authorize?response_type=code&client_id=<client_id>&redirect_uri=<registered redirect URI>&code_challenge=<BASE64URL(SHA256(code_verifier))>&code_challenge_method=S256&state=<opaque value>&resource=<MCP server URL>
Query parameters
| Name | Required | Description |
|---|---|---|
response_type | Yes | Must be code |
client_id | Yes | From registration |
redirect_uri | Yes | One of the client's registered URIs |
code_challenge | Yes | PKCE is mandatory |
code_challenge_method | Yes | S256 only. plain is rejected |
state | Recommended | Returned unchanged on the redirect |
resource | No | The MCP server URL (RFC 8707). It becomes the access token's audience (aud) |
scope | No | Accepted, but it does not narrow the grant (Scopes) |
What the landlord sees
The API returns an HTML page titled Authorize plus the client's name, saying the client is requesting access to the landlord's VIVIN MCP. It never asks for a password: it identifies the landlord from their VIVIN session in that browser.
| Landlord's state | Page |
|---|---|
| Signed in to VIVIN | Signed in as with the email and company, and Sign out to switch accounts. Under "The following tools will be available:", the tools grouped by area, each area marked Read-only or Edit. Buttons Authorize and Cancel |
| Not signed in | "You need to be signed in to VIVIN before you can authorize this connection.", with Sign in to VIVIN and Cancel. After the normal sign-in (emailed code or Google), VIVIN brings the landlord back here |
| No tools enabled | "No MCP tools are enabled for this user yet.", and Authorize is disabled |
A user has tools when their role has the Mcp permission (the Mcp row under Account Settings in Settings → Users → Role Permissions, see Users), AI access is on for them, and at least one area is Read-only or Edit in Settings → AI / MCP → Permissions. Out of the box, only the Admin and Super Admin roles have the Mcp permission.
The consent form expires 15 minutes after it opens.
Result
The API redirects (302) to the redirect_uri with these query parameters:
| Outcome | Parameters |
|---|---|
| Landlord clicks Authorize | code, state. The code works once and expires after 120 seconds |
| Landlord clicks Cancel | error=access_denied, error_description=The landlord denied the authorization request, state |
| No VIVIN session when the form is submitted | error=access_denied, error_description=No VIVIN session. Sign in to VIVIN, then start the connection again. |
| The user has no tools enabled | error=access_denied, error_description=No MCP tools are enabled for this account. Enable at least one tool domain in Settings → AI / MCP first. |
response_type is not code | error=unsupported_response_type, error_description=response_type must be "code" |
code_challenge is missing | error=invalid_request, error_description=code_challenge is required (PKCE is mandatory) |
code_challenge_method is not S256 | error=invalid_request, error_description=code_challenge_method must be S256 |
Errors shown in the browser
When the address is not safe to redirect to, the API shows an HTML error page with status 400 instead:
| Page title | Text | Cause |
|---|---|---|
| Invalid request | "Unknown or missing client_id." | The client_id is not registered |
| Invalid request | "The redirect_uri does not match a registered redirect URI for this client." | The redirect_uri was not registered by that client |
| Session expired | "This authorization form is invalid or has expired. Please start again." | The landlord answered the consent form after 15 minutes |
Token endpoint
POST /oauth/token accepts application/x-www-form-urlencoded or JSON.
Request fields
| Name | Grant | Required | Description |
|---|---|---|---|
grant_type | Both | Yes | authorization_code or refresh_token |
client_id | Both | Yes | From registration |
client_secret | Both | client_secret_post only | From registration |
code | authorization_code | Yes | From the redirect |
redirect_uri | authorization_code | Yes | The same URI as in the authorize request |
code_verifier | authorization_code | Yes | The PKCE verifier |
refresh_token | refresh_token | Yes | The latest refresh token |
Example requests
POST /oauth/token HTTP/1.1
Host: api.vivin.app
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<code>
&redirect_uri=<same redirect URI as in authorize>
&client_id=<client_id>
&code_verifier=<PKCE verifier>
POST /oauth/token HTTP/1.1
Host: api.vivin.app
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=<refresh_token>
&client_id=<client_id>
Response
200 OK, with Cache-Control: no-store:
{
"access_token": "<JWT>",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "<opaque token>",
"scope": "bookings listings properties tenants"
}
| Field | Description |
|---|---|
access_token | A VIVIN JWT. Send it to the MCP server as Authorization: Bearer <access_token> |
expires_in | Seconds until the access token expires, read from the token. Normally 900 (15 minutes): use the value you receive |
refresh_token | An opaque token, valid for 60 days |
scope | The tool areas the user could use when they approved, separated by spaces. Informational only |
Refresh rules
- Rotation: every refresh returns a new refresh token, valid for another 60 days, and the old one stops working. After 60 days without a refresh, the landlord must authorize again.
- Reuse: a refresh token that was already used counts as stolen. VIVIN revokes the whole chain, and the landlord must authorize again. Two requests refreshing the same token at the same moment are different: one succeeds, the other gets
invalid_grant, and nothing is revoked. - Access removed: if the user lost all AI tool access (AI access off, the role lost the Mcp permission, or every area is Off), the refresh fails with
403 access_denied. The refresh token is used up, so send the landlord through authorize again. - User deactivated: the refresh fails with
invalid_grant.
Errors
The register and token endpoints answer RFC 6749 JSON, not the usual VIVIN error body:
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}
| Status | error | error_description | What to do |
|---|---|---|---|
400 | unsupported_grant_type | Unsupported grant_type: <value> (or (missing)) | Send authorization_code or refresh_token |
400 | invalid_request | code is required, code_verifier is required | Send the field |
400 | invalid_request | redirect_uri does not match a registered redirect URI for this client | Send a registered URI |
400 | invalid_grant | Authorization code is invalid, Authorization code has already been used, Authorization code has expired, Authorization code was issued to a different client | Start the authorization again |
400 | invalid_grant | redirect_uri does not match the authorization request, PKCE verification failed, Unsupported PKCE code_challenge_method | Send the same URI and verifier as in authorize |
400 | invalid_grant | refresh_token is required, Refresh token is invalid, Refresh token has expired, Refresh token was issued to a different client | Authorize again |
400 | invalid_grant | Refresh token has been revoked (reuse detected) | Authorize again. Refresh from one place only |
400 | invalid_grant | Refresh token was already used by a concurrent request | Use the token the other request received |
400 | invalid_grant | The authorizing user is no longer active, Account mismatch | The landlord must authorize again |
401 | invalid_client | Unknown client, Client authentication failed | Check client_id and client_secret |
403 | access_denied | MCP access has been revoked for this user | The landlord must restore access, then authorize again |
500 | server_error | Internal server error | Retry later |
Registration errors are listed under Register a client, and authorize errors arrive on the redirect (Result). Past a rate limit, the API answers 429 with the standard VIVIN body (ThrottlerException: Too Many Requests) and a Retry-After header.
Using the access token
- The token only works for the VIVIN AI tools. Send it to the MCP server. On any other VIVIN API route it gets
403withThis access token may only be used for the VIVIN landlord AI tools. - Every tool call is checked against the user's current permissions: their role's Mcp permission, AI access and the areas in Settings → AI / MCP → Permissions. Changes apply to connected clients without a new authorization.
- Write tools are currently limited to Scheduled reports; every other area is read-only. A few VIVIN product-help tools, which return no account data, are always available.
- When the access token expires or is rejected, the MCP server answers
401withWWW-Authenticate: Bearer error="invalid_token". Refresh the token and retry. - The MCP server uses the streamable HTTP transport and returns an
Mcp-Session-Idheader that the client sends on later requests. After a token refresh, or when a session times out, a request on the old session gets404: start a new session. A503withRetry-Aftermeans VIVIN could not check permissions at that moment: wait and retry, do not refresh. Standard MCP clients handle all of this.
Scopes
The scopes are the 14 tool areas of Settings → AI / MCP → Permissions. A grant always covers everything the user is allowed to use: the scope request parameter does not narrow it, and access is enforced by the live permissions, not by the scope string.
| Scope | Area in Settings | Covers |
|---|---|---|
bookings | Bookings | Bookings and their timeline, vacancy, upcoming move-ins, payment plans, sales occupancy and availability |
listings | Listings | Units, calendars, rents and daily rates, manual blocks |
properties | Properties | Properties and their units, smart locks, comments |
tenants | Tenants | Tenants, service requests, surveys and answers |
maintenance | Maintenance | Tickets, today's check-ins and check-outs, check-in/out progress and steps, operations overview, inventory reports and setup |
payments | Payments | Payments, transactions, payouts, deposits, discounts, invoicing settings |
bills | Bills | Utility bills, cost map, cash flows |
owners | Owners | Owners and owner reports |
analytics | Analytics | Revenue, occupancy, debt, finance overview, planning targets |
channels | Channels | Channel and OTA connection status |
communication | Communication | Automatic message rules and messages, notifications, email and WhatsApp conversations |
support | Support | Your support tickets with VIVIN, subscription |
scheduledReports | Scheduled reports | Reports the AI schedules for you |
team | Team | Users and role permissions |
Revoking access
There is no revoke endpoint. To cut off a connected client:
- Switch AI access off for that user under Per-user permissions in Settings → AI / MCP → Permissions. The client's next tool call is refused, and so is its next refresh. Switching it back on before the client refreshes restores the connection.
- Or remove the Mcp permission from the user's role, or set every area to Off.
- Removing the connector in Claude or ChatGPT stops that client. Its refresh token is not revoked, but it expires after 60 days without use.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
invalid_redirect_uri on registration | The client's callback is not on the allowed list. Use an API key from Settings → AI / MCP → API keys |
| The consent page shows no tools and Authorize is disabled | The user lacks the Mcp permission or AI access, or every area is Off |
invalid_grant Authorization code has expired | The code was exchanged more than 120 seconds after the redirect. Exchange it at once |
| The connection breaks after about 60 days | The client never refreshed. Refresh before the access token expires, and keep the newest refresh token |
403 on a VIVIN API route | MCP tokens only work on the MCP server |
Related
- Connect Claude, ChatGPT or other AI tools: the steps for landlords
- AI / MCP settings: the connection URL, API keys, tool permissions and AI access
- Management session: the app's own sign-in, which is separate
- Authentication: partner tokens
- Error handling: error formats on the rest of the API