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"}'

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"
# }

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