Developer Docs

Partner Integration

Integrate OTABot into your PMS, channel manager, or hospitality platform. Offer rate intelligence to your users with just a few lines of code.

How It Works

1

Get Your Credentials

Contact us to receive your API Key (pk_...) and Secret (sk_...). Store the secret securely — never expose it in client-side code.

2

Sign a JWT

On your server, sign a short-lived JWT (60s) with your secret using HS256. The payload must include the user's email.

3

Redirect or Embed

Redirect the user to our login endpoint, or request an embed token and load an iframe. Users are auto-registered on first login.

Redirect Login

Redirect users to the following URL with your API key and signed JWT:

URL
GET https://otabot.com/api/auth/partner-login?key=YOUR_API_KEY&token=YOUR_JWT

OTABot verifies the token, creates a session cookie, and redirects to /dashboard. If the user doesn't have an account, one is created automatically.

javascript
const jwt = require('jsonwebtoken');

// Your partner credentials (from OTABot admin panel)
const API_KEY = 'pk_your_api_key_here';
const SECRET = 'sk_your_secret_here';

function generateOTABotLoginUrl(userEmail) {
  const token = jwt.sign(
    { email: userEmail },
    SECRET,
    { expiresIn: '60s' }
  );

  return `https://otabot.com/api/auth/partner-login?key=${API_KEY}&token=${token}`;
}

// Usage: redirect the user to this URL
const loginUrl = generateOTABotLoginUrl('user@example.com');
// e.g. res.redirect(loginUrl)

Iframe Embedding

Request an embed token from your server, then pass it to an iframe:

URL
GET https://otabot.com/api/auth/partner-embed-token?key=YOUR_API_KEY&token=YOUR_JWT

Response: { "embedToken": "eyJ..." } — valid for 24 hours.

HTML
<iframe
  src="https://otabot.com/embed/dashboard?token=EMBED_TOKEN"
  width="100%" height="800"
  style="border: none; border-radius: 8px;"
></iframe>

Available Pages

PathDescription
/embed/dashboardMonitored URLs and snapshots
/embed/alertsAlert settings and history
/embed/compareSide-by-side competitor comparison
/embed/mapInteractive competitor map
/embed/positioningRate positioning analysis
javascript
const jwt = require('jsonwebtoken');

const API_KEY = 'pk_your_api_key_here';
const SECRET = 'sk_your_secret_here';

async function getEmbedToken(userEmail) {
  const token = jwt.sign(
    { email: userEmail },
    SECRET,
    { expiresIn: '60s' }
  );

  const res = await fetch(
    `https://otabot.com/api/auth/partner-embed-token?key=${API_KEY}&token=${token}`
  );
  const data = await res.json();
  return data.embedToken; // Valid for 24 hours
}

const embedToken = await getEmbedToken('user@example.com');

Customization

Customize embed pages to match your brand by passing color parameters on the iframe URL:

HTML
<iframe
  src="https://otabot.com/embed/dashboard?token=TOKEN&primaryColor=2563EB&bgColor=F0F4FF"
  width="100%" height="800"
  style="border: none; border-radius: 8px;"
></iframe>
  • primaryColor — hex color without #. Replaces the default purple across buttons, links, and accents.
  • bgColor — hex color without #. Changes the page background to match your app.

Colors can also be configured per-partner in the admin panel as defaults, but URL params always take priority.

List Plans

Fetch the available plans and their limits. Useful for displaying plan options in your own UI. Pricing is excluded — you control billing on your side.

URL
GET https://otabot.com/api/partners/plans?key=YOUR_API_KEY

Response:

JSON
{
  "plans": {
    "free": {
      "name": "Free",
      "description": "2 monitored URLs, weekly scans, 1 month look-ahead",
      "maxUrls": 2,
      "lookAheadMonths": 1,
      "scanFrequencyDays": 7,
      "rateHistoryDays": 0,
      "features": ["2 competitor properties", "1 month look-ahead", "..."]
    },
    "essential": { "name": "Essential", "maxUrls": 5, "..." : "..." },
    "growth": { "name": "Growth", "maxUrls": 10, "..." : "..." },
    "business": { "name": "Business", "maxUrls": 15, "..." : "..." }
  }
}

User Management

Partner-managed users don't see OTABot billing or Stripe checkout. You control their plans via API and invoice them however you want.

List Users

URL
GET https://otabot.com/api/partners/users?key=YOUR_API_KEY

Returns all users created through your partner integration:

JSON
{
  "users": [
    {
      "id": "...",
      "email": "user@hotel.com",
      "plan": "growth",
      "billingInterval": "monthly",
      "maxMonitoredUrls": 10,
      "createdAt": "2026-03-20T..."
    }
  ]
}

Set User Plan

URL
PUT https://otabot.com/api/partners/users?key=YOUR_API_KEY

Update a user's plan. The request body should contain:

JSON
{
  "email": "user@hotel.com",
  "plan": "growth",
  "billingInterval": "yearly"
}

Valid plans: free, essential, growth, business. The user's maxMonitoredUrls is automatically set based on the plan.

Optional billingInterval: monthly (default), 6month (1 month free), or yearly (2 months free).

Alert Webhooks

When OTABot detects a significant price change, availability change, competitor undercut, or market shift for one of your users, it can POST a signed JSON payload to an endpoint you control — so you can notify users from your own email domain, push to Slack, drop into a queue, or update your own UI.

Delivery Modes

Every partner account has an alertsHandledByOtabot flag that controls how alerts reach your users:

  • OTABot sends emails (default) — we send white-labeled alert emails directly to your users, using your logo, brand color, display name, and reply-to address.
  • You receive webhooks — we POST the alert data to your webhookUrl and do not send any email. You decide how (and whether) to notify the user.

To switch to webhook delivery, contact us with your webhook URL — we'll set alertsHandledByOtabot=false and register the endpoint on your partner account.

Request Format

Each alert is delivered as a single POST to your endpoint with a JSON body and two signing headers:

HeaderDescription
Content-Typeapplication/json
X-OTABot-SignatureHex-encoded HMAC-SHA256 of `${timestamp}.${body}` using your partner secret
X-OTABot-TimestampUnix timestamp (seconds) — reject requests more than 5 minutes from the current time (past or future) to prevent replays

Payload

The payload bundles every alert detected for a single user in one scan into one delivery. data.changes, data.availabilityChanges, and data.undercutAlerts are always arrays (possibly empty). data.marketShift is an object when a shift was detected and null otherwise.

JSON
{
  "event": "alert_summary",
  "timestamp": "2026-04-21T03:12:04.518Z",
  "hotel": {
    "id": "65f1a2b3c4d5e6f7a8b9c0d1",
    "name": "owner@hotel.com",
    "email": "owner@hotel.com"
  },
  "alert": {
    "type": "alert_summary",
    "severity": "high",
    "message": "3 alerts detected"
  },
  "data": {
    "changes": [
      {
        "propertyName": "Grand Hotel Riviera on Booking.com",
        "date": "2026-05-14",
        "oldPrice": 145,
        "newPrice": 119,
        "percentChange": -17.93
      }
    ],
    "availabilityChanges": [
      { "propertyName": "Seaside Boutique Apt on Airbnb", "date": "2026-05-20" }
    ],
    "undercutAlerts": [
      {
        "competitorName": "Harbor View Suites",
        "competitorPrice": 89,
        "date": "2026-05-14",
        "ownPrice": 120,
        "ownPropertyName": "Your property on Booking.com"
      }
    ],
    "marketShift": {
      "previousAvg": 250,
      "currentAvg": 212.5,
      "shiftPercent": -15
    }
  },
  "email": {
    "subject": "Price changes detected",
    "html": "<html>...white-labeled alert email...</html>",
    "plain_text": "Price changes detected for your monitored properties..."
  },
  "meta": {
    "partner_id": "65e0a1b2c3d4e5f6a7b8c9d0",
    "webhook_version": "1.1"
  }
}

Field Reference

FieldDescription
eventAlways alert_summary — one call bundles all alerts for a user's scan.
hotelThe OTABot user the alerts belong to — id matches the id returned by /api/partners/users, email is the email they log in with.
alert.severityhigh when any price change ≥ 20% or any undercut is present, medium otherwise.
data.changesSignificant price changes on competitor properties. Prices in EUR, percentChange is signed.
data.availabilityChangesCompetitor properties that became unavailable for a specific date.
data.undercutAlertsCompetitors priced below the user's own property on the same date.
data.marketShiftMarket-wide average shift versus the previous scan, if it crossed the user's threshold.
emailOptional pre-rendered white-label email (subject, html, plain_text) using your branding — forward as-is if you just want to send from your own domain. Omitted when there are no price changes to render.
meta.webhook_versionPayload schema version. New fields may be added in minor bumps; breaking changes get a new major version.

Verify & Handle

Verify the signature against the raw request body — not a re-serialized JSON object — and reject any timestamp more than 5 minutes from now:

javascript
const crypto = require('crypto');
const express = require('express');

const SECRET = process.env.OTABOT_SECRET; // 'sk_...' — same secret you use to sign JWTs

const app = express();

// IMPORTANT: use the raw body so the signature verifies byte-for-byte
app.post('/otabot-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.get('X-OTABot-Signature') || '';
  const timestamp = req.get('X-OTABot-Timestamp') || '';
  const body = req.body.toString('utf8');

  // 1. Reject missing/malformed timestamps and requests more than 5 minutes
  //    from now — Number(timestamp) alone returns NaN for bad input, which
  //    silently passes the comparison below.
  const tsSeconds = /^\d+$/.test(timestamp) ? Number(timestamp) : NaN;
  if (!Number.isFinite(tsSeconds) || Math.abs(Date.now() / 1000 - tsSeconds) > 300) {
    return res.status(401).send('Invalid or stale timestamp');
  }

  // 2. Verify HMAC-SHA256 over `${timestamp}.${body}`
  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  const sigBuf = Buffer.from(signature, 'hex');
  const expBuf = Buffer.from(expected, 'hex');
  if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(body);
  // Forward the pre-rendered email from payload.email, push to a queue, etc.
  // Respond 2xx quickly — we don't retry.
  res.status(200).send('ok');
});

Response & Retries

  • Respond with any 2xx status within 10 seconds. Deliveries that time out or return non-2xx are logged but not retried — if you need durability, acknowledge quickly and process asynchronously (e.g. push to a queue).
  • Deliveries are triggered after each daily scan completes for a user, so expect roughly one call per user per day when they have alerts.
  • Your endpoint must be reachable over https://. Treat duplicate deliveries defensively by keying on hotel.id + timestamp if you want idempotency.

Testing

Once your webhook URL is registered, ask us to trigger a test delivery — one click on our side fires a realistic alert_summary payload, signed with your real secret and using your branding, covering all four alert types (price changes, availability, undercut, market shift). It lets you validate signature verification, payload parsing, and your notification flow end-to-end before any real user data flows through.

Test deliveries are tagged with meta.test: true — use it to gate test traffic out of production notifications (e.g. skip actually emailing your user) while still exercising the full verification and handler path.

Price Movement API

Detect market-level price changes near a given location. Pass latitude, longitude, a search radius, and a lookback period — the API compares day-level prices between consecutive scans and returns dates where the market average shifted, grouped by when the change was detected.

URL
GET https://otabot.com/api/partners/price-movement?key=YOUR_API_KEY&lat=37.12&lng=25.24&radiusKm=2&days=14

Parameters

ParameterRequiredDefaultDescription
keyYesYour partner API key
latYesLatitude of the search center
lngYesLongitude of the search center
radiusKmNo0.5Search radius in kilometers (max 50)
daysNo7Lookback period in days (max 14)

Example Response

JSON
{
  "market": [
    {
      "detectedOn": "2026-03-25",
      "changes": [
        {
          "date": "2026-04-15",
          "marketAvgBefore": 220.5,
          "marketAvgAfter": 195.0,
          "changePercent": -11.56
        },
        {
          "date": "2026-05-01",
          "marketAvgBefore": 180.0,
          "marketAvgAfter": 175.0,
          "changePercent": -2.78
        }
      ]
    },
    {
      "detectedOn": "2026-03-28",
      "changes": [
        {
          "date": "2026-04-20",
          "marketAvgBefore": 310.0,
          "marketAvgAfter": 295.0,
          "changePercent": -4.84
        }
      ]
    }
  ],
  "query": { "lat": 37.12, "lng": 25.24, "radiusKm": 2, "days": 14 }
}

The market array groups detected price changes by scan date. Each detectedOn entry contains a changes array listing future dates where the market average shifted between consecutive scans. Prices are in EUR. Only dates with actual price changes are included — no individual property names or details are exposed.

If no monitored properties are found within the radius or no price changes occurred, market will be an empty array.

javascript
const API_KEY = 'pk_your_api_key_here';

async function checkPriceMovement(lat, lng, radiusKm = 0.5, days = 7) {
  const params = new URLSearchParams({
    key: API_KEY,
    lat: String(lat),
    lng: String(lng),
    radiusKm: String(radiusKm),
    days: String(days),
  });

  const res = await fetch(
    `https://otabot.com/api/partners/price-movement?${params}`
  );
  return res.json();
}

// Check market movement within 2km of Naousa, Paros over the last 14 days
const data = await checkPriceMovement(37.1235, 25.2387, 2, 14);
for (const entry of data.market) {
  console.log(`Changes detected on ${entry.detectedOn}:`);
  for (const c of entry.changes) {
    const sign = c.changePercent > 0 ? '+' : '';
    console.log(`  ${c.date}: ${c.marketAvgBefore}€ → ${c.marketAvgAfter}€ (${sign}${c.changePercent}%)`);
  }
}

Security Notes

  • — Never expose your secret in client-side (browser) code
  • — Always generate tokens server-side with a short expiry (60s)
  • — Store your secret in environment variables, not in source code
  • — Each partner JWT should be single-use; generate a fresh one per request
  • — Embed tokens expire after 24 hours — refresh them when needed

Ready to integrate?

Contact us to get your partner credentials and start offering rate intelligence to your users.

Get in Touch