OAuth Connect

Let your users connect their Airbnb account to your app through a hosted, white-label OAuth page. You never handle tokens; we redirect the user back to your URL when they're done.

How it works

From your server, you create a Connect session. We give you a one-time URL on connect.repull.dev. Send your user there. We render a page styled with your brand, walk them through Airbnb's consent screen, exchange the OAuth code for tokens, and redirect them back to your app with ?status=connected appended.

  1. Your server calls POST /v1/connect/airbnb with a redirectUrl.
  2. We return oauthUrl — a URL on connect.repull.dev.
  3. Redirect your user to oauthUrl.
  4. We render your white-label Connect page → Airbnb consent → an account-confirmation screen (“Is this the right Airbnb account?”) → done.
  5. The user lands on your redirectUrl with status query params.

Same model as Stripe Connect or Plaid Link

Your server only mints a session and reads a status. You never touch tokens, never handle the OAuth code, never make a request to Airbnb. The hosted Connect page is rendered with your branding from /dashboard/settings/connect — logos, colors, and trust signals — and includes a mid-flow confirmation step so users can verify they authorized the right Airbnb account before we activate it.

Start a connect session

Server-to-server. Authenticate with your live API key. The session expires after 30 minutes if the user doesn't complete the flow.

curl -X POST 'https://api.repull.dev/v1/connect/airbnb' \
  -H 'Authorization: Bearer sk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "redirectUrl": "https://yourapp.com/airbnb/connected",
    "accessType": "full_access"
  }'

Body parameters

redirectUrlstringRequired

Where we send the user after the OAuth flow completes (or fails). We append status query params to this URL.

accessTypestringDefault: full_access

Permission scope requested from the host. One of full_access or read_only. Determines which Airbnb scopes are requested on the consent screen.

Choosing an access type

  • full_access (default) — read + manage. Recommended for property managers and channel managers that handle messaging, calendars, and pricing on behalf of the host.
  • read_only — read listings, reservations, calendars, messages, and reviews, but no writes back to Airbnb. Recommended for analytics, reporting, and BI tools that just need to pull data.

read_only requests fewer scopes on the Airbnb consent screen — none of the _write permissions are asked for. Hosts see a shorter, less alarming list of permissions, which generally improves conversion for read-only integrations.

Pick once, per host

The access type is locked in at consent time. To change it for an existing connection, the host has to reauthorize through a new Connect session with the new accessType.

Response

{
  "oauthUrl": "https://connect.repull.dev/cs_8gQrT2v9k3M4nLp7wJxYzAbCdEfGhIjKlMnOp",
  "provider": "airbnb",
  "sessionId": "cs_8gQrT2v9k3M4nLp7wJxYzAbCdEfGhIjKlMnOp",
  "expiresAt": "2026-04-29T18:25:14.000Z"
}

Handle the redirect back

When the OAuth flow completes (or fails), we redirect the user to your redirectUrl with these query params appended:

ParamValueMeaning
statusconnectedConnection successful. hostId + accountId are also set.
statuscancelledUser declined or an error occurred. code may explain (access_denied, account_conflict, …).
statusexpiredUser took too long; the link expired before they completed authorization.
hostId123456789Airbnb host ID. Use this to query GET /v1/properties.
accountId42Repull-side connection ID. Stable across token refreshes.
// Your /airbnb/connected handler
public function connected(Request $request)
{
    $status = $request->query('status');

    if ($status === 'connected') {
        $hostId    = $request->query('hostId');     // Airbnb host ID
        $accountId = $request->query('accountId');  // Repull connection ID
        // Associate $hostId with the currently authenticated user, kick off
        // any post-connect work in your app (welcome email, dashboard nudge).
        return redirect()->route('dashboard')->with('success', 'Airbnb connected!');
    }

    if ($status === 'cancelled') {
        $code = $request->query('code', 'unknown');
        $message = match($code) {
            'wrong_account'    => 'You authorized the wrong Airbnb account. Try again.',
            'access_denied'    => 'You declined to authorize Airbnb.',
            'account_conflict' => 'This Airbnb account is already connected elsewhere.',
            default            => "Connection failed: {$code}",
        };
        return redirect()->route('dashboard.connections')->with('error', $message);
    }

    if ($status === 'expired') {
        return redirect()->route('dashboard.connections')->with('error', 'The connect link expired. Please try again.');
    }

    return redirect()->route('dashboard.connections');
}

Associating the connection with your user

When the user lands back on your redirectUrl, you'll usually want to record hostId against the user in your database. Two ways to know who that user is:

Option 1 — Auth-required redirect (simplest)

If your redirectUrlrequires login, the user's session is intact when they land back — the OAuth round-trip happens in the same browser session. Your usual auth()->user() / request.user works.

Option 2 — Encode user context into the redirectUrl

We preserve all your existing query params. When the user lands back, we only append &status=...&hostId=...&accountId=... to the URL you gave us:

// Generate
$signed = hash_hmac('sha256', $userId . ':' . time(), config('app.key'));
'redirectUrl' => route('airbnb.connected', ['from' => $userId, 'sig' => $signed]),

// Receive
public function connected(Request $request)
{
    $userId = (int) $request->query('from');
    $sig    = $request->query('sig');
    if (! $this->verify($userId, $sig)) abort(403);

    $hostId = $request->query('hostId');
    User::find($userId)->airbnbHosts()->syncWithoutDetaching([$hostId]);
    // ...
}

Why sign it?

Without a signature, anyone could craft a URL with a different from value and link an Airbnb host to the wrong user. A signed nonce or short-lived token makes this safe.

Check connection status anytime

After the user is connected, query the canonical state via GET /v1/connect/airbnb:

curl 'https://api.repull.dev/v1/connect/airbnb' \
  -H 'Authorization: Bearer sk_live_...'

# {
#   "connected": true,
#   "provider": "airbnb",
#   "id": 42,
#   "status": "active",
#   "externalAccountId": "123456789",
#   "createdAt": "2026-04-29T18:01:42.000Z",
#   "host": {
#     "displayName": "Lidia",
#     "displayNameLong": "Lidia",
#     "avatarUrl": "https://a0.muscache.com/im/users/.../profile_pic.jpg",
#     "avatarUrlLarge": "https://a0.muscache.com/im/users/.../profile_pic_large.jpg",
#     "activationStatus": "active"
#   }
# }

After a successful connection, the response includes a hostobject with the host's display name and Airbnb avatar so you can render an account-level confirmation UI in your own app — no follow-up calls required.

Why no host email?

Airbnb's partner API does not expose host email to integrators, so we can't return one. The host's account email is the email they used to sign in to your app — you already have that. host.displayName + host.avatarUrlis what you need to confirm “you linked the right account” back to the user.

For non-Airbnb providers (Booking, VRBO, Plumguide), host is currently null. Per-provider enrichment will land incrementally.

Failure modes

All non-success outcomes redirect back to your redirectUrl with ?status=cancelled&code=<reason> (or ?status=expired):

  • access_denied — user clicked “Cancel” on Airbnb's consent screen.
  • wrong_account — user clicked “Use a different account” on the confirmation screen. They likely had the wrong Airbnb account logged in. Mint a fresh session and ask them to retry (in another browser tab where they're logged in to the right account).
  • account_conflict — the Airbnb account is already connected to a different Repull workspace. Disconnect it from there first.
  • token_exchange_failed — Airbnb rejected our token exchange. Usually transient; retry.
  • activation_failed — DB write failed during the final activate step. Rare; transient; retry.
  • expired (status, not code) — connect link is older than 30 minutes. Generate a new one.

Webhooks

Want to be notified the instant a connection completes? Subscribe to connection.created via webhooks. Coming soon.

Custom domain (Scale plan)

On the Scale plan, you can swap connect.repull.dev for your own subdomain — e.g. connect.yourapp.com — so the URL bar matches your brand end-to-end. Add a CNAME to cname.vercel-dns.com, paste the hostname in your dashboard, and we'll provision SSL automatically.

View Scale pricing →

AI