Appearance
OAuth install
The OAuth install flow lets hosts connect their Stayblox account to your app from your own website — a "Connect your Stayblox account" button. After authorization, you receive a per-install API token identical to the one generated by a marketplace install.
Who can use this: OAuth install is available for free and externally-billed apps only. If your app is paid and platform-billed (Stayblox charges the host's card), it must be installed from the marketplace — that's where the billing transaction lives.
The flow at a glance
Host visits your site and clicks "Connect Stayblox"
│
Redirect to GET /oauth/authorize (with client_id, scope, redirect_uri, state)
│
Stayblox: log in if needed → team picker → consent screen
│
Redirect back to redirect_uri?code=...&state=...
│
Your server: POST /oauth/token (client_id, client_secret, code, redirect_uri)
│
Response: { access_token, token_type, scope }
│
Store the access_token for this install → call the Developer APIStep 1: Authorization request
Send the host to:
GET https://app.stayblox.com/oauth/authorizeRequired parameters:
| Parameter | Description |
|---|---|
response_type | Must be code. |
client_id | Your app's client ID (from the Developer panel → Credentials). |
redirect_uri | Must exactly match one of the oauth.redirect_uris registered in your manifest. Any mismatch returns a 400 error — no redirect. |
state | An opaque value you generate. Returned unchanged in the redirect. Use it to prevent CSRF attacks and to tie the callback to your session. |
Optional parameters:
| Parameter | Description |
|---|---|
scope | Space-separated subset of your manifest's declared scopes. If omitted, all declared scopes are requested. Requesting a scope not in the manifest returns 400. |
Example link:
https://app.stayblox.com/oauth/authorize
?response_type=code
&client_id=sb_app_01JXXXXXXXXX
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fstayblox%2Fcallback
&scope=read_bookings+write_conversations
&state=xyzrandomstate123Step 2: Consent and team selection
Stayblox:
- Prompts the host to log in if they aren't already authenticated.
- Shows a team picker — the host selects which team (property portfolio) to connect.
- Shows the consent screen — a human-readable list of the requested scopes.
The host can click Deny, which redirects back to your redirect_uri with error=access_denied.
Step 3: Authorization callback
On approval, Stayblox redirects to your redirect_uri:
https://app.example.com/auth/stayblox/callback
?code=AUTH_CODE_HERE
&state=xyzrandomstate123Always verify state matches what you sent in step 1 before proceeding. A mismatch indicates a CSRF attack; discard the request.
The code is single-use and expires in 10 minutes.
Step 4: Token exchange
Exchange the code for an access token:
POST https://app.stayblox.com/oauth/token
Content-Type: application/x-www-form-urlencoded| Parameter | Description |
|---|---|
grant_type | Must be authorization_code. |
client_id | Your app's client ID. |
client_secret | Your app's client secret (from the Developer panel → Credentials). Never expose this client-side. |
code | The authorization code from step 3. |
redirect_uri | Must exactly match the URI used in step 1. |
bash
curl -s -X POST https://app.stayblox.com/oauth/token \
-d grant_type=authorization_code \
-d client_id=sb_app_01JXXXXXXXXX \
-d client_secret=$STAYBLOX_CLIENT_SECRET \
-d code=AUTH_CODE_HERE \
-d redirect_uri=https://app.example.com/auth/stayblox/callbackSuccess response (200):
json
{
"access_token": "1|xxxxxxxxxxxxxxxxxxxxxx",
"token_type": "Bearer",
"scope": "read_bookings write_conversations"
}access_token is a Sanctum bearer token. Store it securely — treat it like a password. Use it as Authorization: Bearer <token> on all Developer API calls. The scope field reflects the scopes the host actually consented to (may be a subset if they declined some).
Error responses:
| Error code | Meaning |
|---|---|
invalid_grant | Code is invalid, expired (> 10 min), already used, or the client_id / redirect_uri doesn't match what the code was issued for. |
invalid_client | client_secret doesn't match. |
unsupported_grant_type | grant_type is not authorization_code. |
unauthorized_client | Paid platform-billed app — must install from the marketplace. |
Reconnects and re-consent
If the same host reconnects (they hit your authorize URL again for a team they've already installed on), Stayblox merges scopes rather than creating a duplicate install. The new scopes are unioned with the existing grants, and a fresh token is returned. The existing token for that install remains valid until explicitly revoked.
Complete example (Node.js)
js
import express from 'express'
import crypto from 'node:crypto'
const app = express()
// 1. Generate the authorization URL and redirect the host.
app.get('/auth/stayblox', (req, res) => {
const state = crypto.randomBytes(16).toString('hex')
req.session.oauthState = state // store in session to verify later
const params = new URLSearchParams({
response_type: 'code',
client_id: process.env.STAYBLOX_CLIENT_ID,
redirect_uri: 'https://app.example.com/auth/stayblox/callback',
scope: 'read_bookings write_conversations',
state,
})
res.redirect(`https://app.stayblox.com/oauth/authorize?${params}`)
})
// 2. Handle the callback.
app.get('/auth/stayblox/callback', async (req, res) => {
const { code, state, error } = req.query
if (error === 'access_denied') {
return res.redirect('/?error=cancelled')
}
// CSRF check.
if (state !== req.session.oauthState) {
return res.status(400).send('State mismatch.')
}
// 3. Exchange the code for a token.
const response = await fetch('https://app.stayblox.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.STAYBLOX_CLIENT_ID,
client_secret: process.env.STAYBLOX_CLIENT_SECRET,
code,
redirect_uri: 'https://app.example.com/auth/stayblox/callback',
}),
})
if (!response.ok) {
const err = await response.json()
return res.status(400).send(`OAuth error: ${err.error}`)
}
const { access_token, scope } = await response.json()
// Store the token associated with this host/user.
await db.storeToken({ userId: req.session.userId, token: access_token, scope })
res.redirect('/dashboard?connected=true')
})Security notes
- Never include
client_secretin client-side code or a public repository. The token exchange must happen server-side. - Always verify
statein the callback to prevent CSRF. - Use
https://redirect URIs in production. The manifest validator rejects non-HTTPS URIs. - Rotate your client secret from the Developer panel if it is ever exposed. Existing tokens remain valid after rotation; new OAuth exchanges use the new secret immediately.
- Codes are single-use. Attempting to use a code twice returns
invalid_grant.