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, webhook_secret }

 Store the access_token for this install → call the Developer API

Step 1: Authorization request

Send the host to:

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

Required parameters:

ParameterDescription
response_typeMust be code.
client_idYour app's client ID (from your account dashboard).
redirect_uriMust exactly match one of the oauth.redirect_uris declared in your app.toml (under [oauth]). Any mismatch returns a 400 error with 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 the scopes declared in your app.toml. If omitted, all declared scopes are requested. Requesting a scope not in app.toml returns 400.

Example link:

https://admin.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://admin.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 your account dashboard). 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://admin.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",
  "webhook_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

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).

webhook_secret is the per-install secret used to verify the HMAC signature on inbound webhook deliveries from Stayblox. Store it alongside the access_token and key it by install so one server can serve many hosts. See Signing & security for the verification algorithm. Never expose this value client-side or include it in a public repository.

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://admin.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://admin.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, webhook_secret } = await response.json()

  // Store the token and webhook secret associated with this install.
  // webhook_secret is used to verify inbound webhook signatures. Treat it like a password.
  await db.storeToken({ userId: req.session.userId, token: access_token, scope, webhookSecret: webhook_secret })

  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 your account dashboard 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