Skip to content

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 API

Step 1: Authorization request

Send the host to:

GET https://app.stayblox.com/oauth/authorize

Required parameters:

ParameterDescription
response_typeMust be code.
client_idYour app's client ID (from the Developer panel → Credentials).
redirect_uriMust exactly match one of the oauth.redirect_uris registered in your manifest. Any mismatch returns a 400 error — no redirect.
stateAn 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:

ParameterDescription
scopeSpace-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=xyzrandomstate123

Stayblox:

  1. Prompts the host to log in if they aren't already authenticated.
  2. Shows a team picker — the host selects which team (property portfolio) to connect.
  3. 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=xyzrandomstate123

Always 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
ParameterDescription
grant_typeMust be authorization_code.
client_idYour app's client ID.
client_secretYour app's client secret (from the Developer panel → Credentials). Never expose this client-side.
codeThe authorization code from step 3.
redirect_uriMust 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/callback

Success 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 codeMeaning
invalid_grantCode is invalid, expired (> 10 min), already used, or the client_id / redirect_uri doesn't match what the code was issued for.
invalid_clientclient_secret doesn't match.
unsupported_grant_typegrant_type is not authorization_code.
unauthorized_clientPaid platform-billed app — must install from the marketplace.

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_secret in client-side code or a public repository. The token exchange must happen server-side.
  • Always verify state in 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.

© Stayblox — Developer Platform