Management session authentication
A management session lets your own script sign in as one of your VIVIN users and call the API with that user's access, the same way the VIVIN web app does.
Before you start
| Item | Value |
|---|---|
| Who it is for | Landlords and property managers automating their own account. Booking platforms use a partner token; AI clients use MCP OAuth |
| Base URL | https://api.vivin.app |
| You need | A VIVIN user with a password, and access to that user's mailbox for the emailed code |
| Access | Limited to the user's account and to their role's permissions in Settings → Users → Role Permissions |
| Routes | The routes the VIVIN web app uses. They are not versioned, and VIVIN does not publish a reference for them |
| Management session | Partner token | |
|---|---|---|
| Who uses it | Your VIVIN users, the web app and your own scripts | Booking platforms and other partners |
| How you get it | Email and password, then a code sent by email | VIVIN issues one per platform |
| What it opens | The app's routes, limited to your account and the user's role | Only partner routes (/<prefix>/…) |
| Lifetime | A 15-minute access token, renewed with a refresh cookie, for up to 30 days | Set by VIVIN when it issues the token |
A partner token does not work on management routes, and a management token does not work on partner routes.
Endpoints
| Method | Path | What it does |
|---|---|---|
POST | /auth/login | Checks the email and password, then emails a code |
POST | /auth/verify-otp | Checks the code and starts the session |
POST | /auth/resend-otp | Emails a new code for the same sign-in |
GET | /auth/me | Returns the signed-in user, their account and preferences |
POST | /auth/refresh | Exchanges the refresh cookie for a new access token |
POST | /auth/logout | Ends the session |
Sign in
POST /auth/login checks the password. Usually it then emails a 6-digit code to the user.
Request fields
| Name | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The user's email address |
password | string | Yes | At least 6 characters |
Example request
curl -sS -b vivin-cookies.txt -c vivin-cookies.txt \
-X POST https://api.vivin.app/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"your-password"}'
Response
201 Created, asking for the code:
{
"otpRequired": true,
"challengeToken": "<challenge-token>",
"expiresAt": "2026-09-26T10:10:00.000Z",
"maskedEmail": "y••@example.com"
}
- The code is valid for 10 minutes. Signing in again cancels the earlier code.
- When the request carries this user's remembered-browser cookie (Verify the code), there is no code: the response is already the session, with the body shown under Verify the code, and the refresh cookie is set.
Errors
| Status | message | Cause | What to do |
|---|---|---|---|
400 | email must be an email, password must be longer than or equal to 6 characters | A malformed field. Unknown fields also return 400 | Fix the body |
401 | Invalid credentials | Wrong email or password, or the user never set a password | Check them. A user without a password sets one with Forgot password? |
401 | Account deactivated | The user is deactivated | Ask an admin to reactivate the user |
429 | Too many sign-in attempts for this account. Wait a few minutes and try again. | Too many wrong passwords for this email, from your network or overall, in 15 minutes. The body adds retryAfterSeconds and errorCode: "TOO_MANY_LOGIN_ATTEMPTS" | Wait retryAfterSeconds, then sign in again |
429 | Too many login codes requested. Please wait before trying again. | 5 codes were already sent to this user in the last 15 minutes | Wait a few minutes |
429 | ThrottlerException: Too Many Requests | More than 10 sign-in requests from your IP address in a minute | Wait the seconds in the Retry-After header |
Verify the code
POST /auth/verify-otp completes the sign-in with the emailed code.
Request fields
| Name | Type | Required | Description |
|---|---|---|---|
challengeToken | string | Yes | The value from POST /auth/login |
otp | string | Yes | The 6-digit code from the email, as a string |
rememberDevice | boolean | No | true sets a remembered-browser cookie, so later sign-ins with the same cookie jar skip the code for 30 days. It matches Don't ask for a code on this browser for 30 days on the Check your email screen |
Example request
curl -sS -b vivin-cookies.txt -c vivin-cookies.txt \
-X POST https://api.vivin.app/auth/verify-otp \
-H "Content-Type: application/json" \
-d '{"challengeToken":"<challenge-token>","otp":"481920","rememberDevice":true}'
Response
200 OK with the session:
{
"token": "<access-token>",
"user": {
"id": "…",
"email": "[email protected]",
"firstName": "Casey",
"lastName": "Sample",
"role": "admin",
"accountId": "…",
"ownerId": null,
"hasProperties": null
},
"account": { "id": "…", "companyName": "…", "logoUrl": "…" }
}
| Field | Description |
|---|---|
token | The access token for your API calls, valid for 15 minutes |
user | The signed-in user. ownerId and hasProperties are filled only for owner users |
account | The user's account |
The response also sets an httpOnly refresh cookie. Keep it: you need it to refresh.
Errors
Code errors carry a reason next to the usual fields:
{
"reason": "invalid_code",
"statusCode": 401,
"message": "That code is not valid. Request a new one and try again.",
"error": "UnauthorizedException"
}
| Status | message and reason | Cause | What to do |
|---|---|---|---|
400 | otp must contain digits only, otp must be longer than or equal to 6 characters and similar | A malformed field | Send the 6 digits as a string |
401 | That code is not valid. Request a new one and try again., reason: invalid_code | Wrong code. Each code allows 5 tries | Check the code and send it again |
401 | Same message, reason: too_many_attempts, expired, consumed or not_found | This sign-in attempt is over | Start again at POST /auth/login |
401 | Session is no longer valid | The user was deactivated between the two steps | Ask an admin |
429 | ThrottlerException: Too Many Requests | More than 10 requests from your IP address in a minute | Wait the seconds in Retry-After |
Send a new code
POST /auth/resend-otp emails a new code for the same sign-in, like Send a new code in the app. The new code replaces the old one and gets a fresh 10 minutes and 5 tries, up to 3 times per sign-in.
Request fields
| Name | Type | Required | Description |
|---|---|---|---|
challengeToken | string | Yes | The value from POST /auth/login |
Response
200 OK with { "message": "A new code is on its way.", "expiresAt": "<timestamp>" }.
Errors
| Status | message and reason | Cause | What to do |
|---|---|---|---|
401 | That sign-in attempt has expired. Go back and enter your password again., reason: too_many_resends | Three new codes were already sent | Use the last code, or start again at POST /auth/login |
401 | Same message, reason: expired, too_many_attempts, consumed or not_found | This sign-in attempt is over | Start again at POST /auth/login |
429 | ThrottlerException: Too Many Requests | More than 5 requests from your IP address in a minute | Wait the seconds in Retry-After |
Call the API
Send the access token in the Authorization header of every request.
curl https://api.vivin.app/auth/me \
-H "Authorization: Bearer <access-token>"
GET /auth/me returns the signed-in user, their account and preferences: a good first call to check the token.
Most routes and GET /auth/me word their 401s differently:
| Status | message (most routes) | message (/auth/me) | Cause | What to do |
|---|---|---|---|---|
401 | Missing Authorization header | Missing authentication credentials | No header | Add it |
401 | Invalid Authorization header format | Invalid authentication credentials | The header is not Bearer <token> | Fix the header |
401 | Unauthorized | Invalid authentication credentials | The token expired or is not valid | Refresh, then retry once |
401 | Your session has ended. | Your session has ended. | The session was signed out, or the user's password was changed or reset | Sign in again |
403 | Forbidden resource | — | The user's role lacks the permission the route needs, or the user was deactivated | Ask an admin |
Refresh the access token
The access token lasts 15 minutes. POST /auth/refresh, with the refresh cookie and no body, returns a new one.
curl -sS -b vivin-cookies.txt -c vivin-cookies.txt \
-X POST https://api.vivin.app/auth/refresh
Response
201 Created with the same body as Verify the code and a new token. It also replaces the refresh cookie.
- Save the new cookie every time: each refresh cookie works once.
- A replaced refresh cookie sent again more than 30 seconds later is treated as stolen and ends the session. Within 30 seconds (two calls racing), the second call also succeeds.
- A session ends 30 days after the sign-in, however often you refresh. Then sign in again, with the code unless the remembered-browser cookie is still valid.
- An empty body sent with
Content-Type: application/jsonis accepted.
Errors
| Status | message | Cause | What to do |
|---|---|---|---|
401 | No refresh token | The cookie was not sent | Send the cookie jar |
401 | Invalid or expired refresh token | The cookie is unknown, expired, or the 30 days are over | Sign in again |
401 | Refresh token reuse detected | A replaced cookie was sent again: the session is ended | Sign in again |
401 | Session is no longer valid | The user was deactivated or removed | Ask an admin |
429 | ThrottlerException: Too Many Requests | More than 60 refreshes from your IP address in a minute | Wait and retry |
Sign out
POST /auth/logout, with the refresh cookie and no body, ends the session and clears the cookie. It always returns 200 with {"message":"Logged out"}, even when the access token has already expired.
Google sign-in
The Google button on the VIVIN sign-in page uses a Google ID token issued for VIVIN's own page. It skips the emailed code and only signs in users who already exist in VIVIN. Scripts cannot get such a token, so they sign in with email and password; a user who signs in only with Google sets a password first with Forgot password?.
Full example
This bash script signs in, asks for the emailed code, calls the API, refreshes the token and signs out. It needs curl and jq.
API=https://api.vivin.app
JAR=vivin-cookies.txt # holds the session: keep it private
# 1. Email and password (sends the remembered-browser cookie, if any)
curl -sS -b "$JAR" -c "$JAR" -X POST "$API/auth/login" \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"your-password"}' > session.json
# 2. The emailed code, only when VIVIN asks for it
if [ "$(jq -r '.otpRequired // false' session.json)" = "true" ]; then
read -r -p "Code sent to $(jq -r .maskedEmail session.json): " CODE
jq -n --arg t "$(jq -r .challengeToken session.json)" --arg c "$CODE" \
'{challengeToken: $t, otp: $c, rememberDevice: true}' \
| curl -sS -b "$JAR" -c "$JAR" -X POST "$API/auth/verify-otp" \
-H 'Content-Type: application/json' -d @- > session.json
fi
TOKEN=$(jq -r .token session.json)
# 3. Call the API
curl -sS "$API/auth/me" -H "Authorization: Bearer $TOKEN"
# 4. After 15 minutes, get a new access token
TOKEN=$(curl -sS -b "$JAR" -c "$JAR" -X POST "$API/auth/refresh" | jq -r .token)
# 5. End the session
curl -sS -b "$JAR" -c "$JAR" -X POST "$API/auth/logout"
Limits
| Route | Requests per IP address and minute |
|---|---|
POST /auth/login, /auth/verify-otp | 10 |
POST /auth/resend-otp | 5 |
POST /auth/logout | 30 |
POST /auth/refresh | 60 |
| Most other management routes | 600 |
Past a limit, the API answers 429 ThrottlerException: Too Many Requests with a Retry-After header. Wrong passwords also count per email address, across networks (Sign in).
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
| Every sign-in from your script asks for a code | The cookie jar is not kept between runs, or rememberDevice was not true. Keep the jar private and reuse it |
Refresh token reuse detected after a while | Two machines or processes share one refresh cookie. Refresh from one place only |
Your session has ended. on every call | The user's password was changed or reset, which ends every session and forgets remembered browsers. Sign in again |
403 Forbidden resource | The user's role does not allow that action. Use a user with the right role |
Invalid credentials for a user who signs in with Google | The user has no password. Set one with Forgot password? |
Related
- Authentication: partner tokens for booking platforms
- MCP OAuth: how Claude and ChatGPT sign in to VIVIN
- Connect AI tools: connecting an AI client to your account
- Users: role permissions that limit each session
- Error handling: the error body and status codes