Open Embed Pabbly Connect as Your Connection Manager — REST API & SDK Integration Guide

KamalK

Member
Staff member
Markdown (GitHub flavored):
# Embed Pabbly Connect as Your Connection Manager — REST API & SDK Integration Guide

Add **Pabbly Connect** as the connection manager inside your own product. Let your
users connect 1,000+ apps (Gmail, Slack, Google Sheets, …) through Pabbly's hosted
OAuth / credential flows, then fetch **always-fresh access tokens** from your backend
to call those apps on your users' behalf.

This single guide covers everything an engineer needs: the concepts, the end-to-end
flow, the **Node SDK** (recommended), the **raw REST API** (for any other language),
the **browser popup helper**, the optional **MCP server** for AI agents, the security
model, error handling, and a go-live checklist.

> **The golden rule:** your **tenant key** and any **vended tokens** live **only on
> your backend** — never in a browser, a mobile app, or an LLM context.

---

## Table of contents

1. [What you're building](#1-what-youre-building)
2. [Core concepts in 60 seconds](#2-core-concepts-in-60-seconds)
3. [Get your tenant key](#3-get-your-tenant-key)
4. [Authentication & the end-user header](#4-authentication--the-end-user-header)
5. [The integration flow at a glance](#5-the-integration-flow-at-a-glance)
6. [Quickstart with the Node SDK](#6-quickstart-with-the-node-sdk)
7. [SDK reference](#7-sdk-reference)
8. [REST API reference (any language)](#8-rest-api-reference-any-language)
9. [The frontend popup (browser helper)](#9-the-frontend-popup-browser-helper)
10. [Agent / MCP access (optional)](#10-agent--mcp-access-optional)
11. [Security & visibility model](#11-security--visibility-model)
12. [Errors & rate limits](#12-errors--rate-limits)
13. [Production checklist](#13-production-checklist)
14. [FAQ](#14-faq)

---

## 1. What you're building

You have a product. Your users want it to *do things in other apps* — send a Gmail,
update a Google Sheet, post to Slack. Building and maintaining OAuth for each of those
providers is a large, ongoing effort.

With the Pabbly Connect Platform API, you offload all of that:

- Your user clicks **"Connect Gmail"** in *your* UI.
- Pabbly Connect shows the hosted authorization screen and securely stores the grant.
- Your backend asks Pabbly for a **fresh access token** whenever it needs to act, and
  calls Gmail directly.

You never see or store the user's third-party password, and you never run the OAuth
refresh machinery yourself.

---

## 2. Core concepts in 60 seconds

| Term | What it means |
|------|----------------|
| **Tenant** | You — the integrating product. You hold a secret **tenant key** (`pk_live_…`). A tenant is itself a Pabbly Connect user account. |
| **End user (customer)** | A person in *your* product, identified to Pabbly by **email**. A lightweight Connect user is created/tracked per email automatically. |
| **Connection** | One app authorization owned by one end user (e.g. *"Alice's Gmail"*). Every connection you create is **stamped with your tenant** and visible **only to you**. |
| **Request link** | A hosted URL where the end user completes OAuth or enters credentials. You never handle their secrets. |
| **Token vending** | Your backend exchanges a connection for a currently-valid access token (refreshed on demand) to call the third-party API. |

---

## 3. Get your tenant key

A key is issued in one of two ways:

- **Self-service** (a key for your own account): Connect dashboard →
  **Settings → Platform API Keys → Generate Credentials**. The key is shown **once** —
  store it immediately in your secret manager.
- **Product tenant** (a dedicated account that serves many of your customers):
  provisioned by Pabbly ops. This is what a multi-customer SaaS integration uses.

### Capabilities

A key can be granted any subset of these capabilities:

| Capability | Grants access to |
|------------|------------------|
| `apps` | Browse the app catalog |
| `connections` | Create/list/read connections |
| `token` | Vend access tokens |
| `delete` | Delete connections |

Calling an endpoint group your key wasn't granted returns `403 capability_disabled`.

### Key rotation (zero-downtime)

Rotation mints a **second** key (both work simultaneously), then **promote** retires
the old one:

```
rotate  →  deploy the new key everywhere  →  promote (retires the old key)
```

---

## 4. Authentication & the end-user header

Every request carries your tenant key as a Bearer token:

```
Authorization: Bearer pk_live_xxxxxxxx…
```

Every **`/connections`** call **must also** name the customer it acts on:

```
x-user-email: [email protected]
```

> **`x-user-email` is required** on all REST/SDK connection calls — you are always
> acting on behalf of a specific customer. Source the email from **your own
> authenticated session**, never from unverified user input. A missing/invalid header
> returns `400 invalid_user_email`.
>
> The **only** exception is the header-less **MCP URL** (see §10), where clients that
> can't send headers fall back to the tenant's own account.

The `/apps` catalog endpoints need **no** email.

---

## 5. The integration flow at a glance

```
            your backend                          your frontend          Pabbly Connect
1. listApps ───────────────────────────────────────────────────────────────▶ catalog
2. createRequestLink(email, app) ──────────────────────────────────────────▶ pending connection
   ◀── { connection_id, request_token, request_link } ──
3.                               hand request_link ──▶ open popup ──────────▶ user authorizes
4. waitForConnection / poll getConnection(email, id) ──────────────────────▶ status: active
5. getToken(email, id) ────────────────────────────────────────────────────▶ { access_token, … }
   └─ call Gmail / Slack / … with the token
```

Steps **1, 2, 4, 5** are backend (use the tenant key). Step **3** is the only thing
your frontend touches — and it only ever sees the `request_link` / `request_token` /
`connection_id`, never your tenant key.

---

## 6. Quickstart with the Node SDK

The Node SDK is the fastest path. Requires **Node ≥ 18**.

```bash
npm install @pabbly/connect-platform
```

```js
const {
  PabblyConnectPlatform,
  NeedsReconnectionError,
  RateLimitedError,
} = require('@pabbly/connect-platform');

const pc = new PabblyConnectPlatform({ apiKey: process.env.PC_TENANT_KEY });
const customer = '[email protected]';   // REQUIRED on every connection call

// 1 + 2 — find the app, create a hosted link for this customer
const { apps } = await pc.listApps({ search: 'gmail' });
const link = await pc.createRequestLink(customer, {
  appId: apps[0].id,
  name: 'Work Gmail',
});
// → { connection_id, request_token, request_link, expires_at, app }

// 3 — hand link.request_link + request_token + connection_id to your frontend (see §9)

// 4 — wait for the user to finish (resolves when status leaves 'pending')
const conn = await pc.waitForConnection(customer, link.connection_id);
if (conn.status !== 'active') {
  // 'rejected' | 'expired' → start over with a new link
}

// 5 — at runtime, get a working credential and call the third-party API
try {
  const token = await pc.getToken(customer, link.connection_id);

  if (token.access_token) {                     // OAuth app
    await fetch('https://gmail.googleapis.com/...', {
      headers: { Authorization: `${token.token_type} ${token.access_token}` },
    });
  } else {                                      // API-key / basic / header app
    useFields(token.fields);                    // keyed by token.auth_type
  }
} catch (err) {
  if (err instanceof NeedsReconnectionError) {
    // grant revoked/expired → send the user through the connect flow again
  } else if (err instanceof RateLimitedError) {
    await sleep(err.retryAfter * 1000);         // then retry
  } else {
    throw err;
  }
}
```

---

## 7. SDK reference

Construct once, reuse everywhere:

```js
const pc = new PabblyConnectPlatform({
  apiKey: process.env.PC_TENANT_KEY,   // pk_live_…  (required)
  baseUrl: undefined,                  // optional override
  maxRetries: 2,                       // retries 503 only, with backoff
  timeoutMs: 30000,
});
```

### Methods

| Method | Notes |
|--------|-------|
| `listApps({ search?, page?, limit? })` | App catalog. **No email needed.** |
| `getApp(appId)` | Fetch a single app. No email. |
| `createRequestLink(email, { appId, name })` | → `{ connection_id, request_token, request_link, expires_at, app }` |
| `getConnection(email, id)` | Status: `pending` / `active` / `rejected` / `expired` |
| `waitForConnection(email, id, { intervalMs?, timeoutMs? })` | Resolves when status leaves `pending` |
| `getToken(email, id)` | OAuth → `{ access_token, token_type, expires_in, auth_type }`; else `{ auth_type, fields }`. **Never persist or log.** |
| `listConnections(email, { appId?, page?, limit? })` | Only connections **your tenant** created |
| `deleteConnection(email, id)` | Needs `delete` capability |

> **Every connection method requires `email` as the first argument** and throws
> synchronously if you omit it — that's intentional. `listApps` / `getApp` do not.

### Retry & error behavior

The SDK automatically retries **only `503`** (transient upstream) with backoff.
Everything else is surfaced as a **typed error** subclass of `PabblyConnectError`,
each carrying `.status`, `.code`, and `.response`:

| Error class | When |
|-------------|------|
| `NeedsReconnectionError` | 401 — grant revoked/expired; re-run the connect flow |
| `CapabilityDisabledError` | 403 — your key lacks that capability |
| `ConnectionDeletedError` | 404 — unknown connection (or not yours) |
| `RateLimitedError` | 429 — honor `.retryAfter` (seconds) |
| `UpstreamUnavailableError` | 503 — auto-retried twice before surfacing |

---

## 8. REST API reference (any language)

If you're not on Node, call the REST API directly.

**Base URL:** `https://connect.pabbly.com/platform/v1`

All responses share the shape `{ status, message, data }`. IDs are **opaque strings** —
store them exactly as returned.

| Endpoint | Needs `x-user-email` | Capability |
|----------|:--------------------:|:----------:|
| `GET /apps?search=&page=&limit=` | no | `apps` |
| `GET /apps/{appId}` | no | `apps` |
| `POST /connections/request-link` — body `{app_id, name}` | **yes** | `connections` |
| `GET /connections?app_id=&page=&limit=` | **yes** | `connections` |
| `GET /connections/{id}` | **yes** | `connections` |
| `POST /connections/{id}/token` | **yes** | `token` |
| `DELETE /connections/{id}` | **yes** | `delete` |

### Example calls

```bash
# 1. Find the app
curl 'https://connect.pabbly.com/platform/v1/apps?search=gmail&limit=5' \
  -H 'Authorization: Bearer pk_live_…'

# 2. Create a request link for a customer
curl -X POST https://connect.pabbly.com/platform/v1/connections/request-link \
  -H 'Authorization: Bearer pk_live_…' \
  -H 'x-user-email: [email protected]' \
  -H 'Content-Type: application/json' \
  -d '{"app_id":"<id from /apps>","name":"Work Gmail"}'
# → { connection_id, request_token, request_link, expires_at, app }

# 4. Poll status until "active"
curl https://connect.pabbly.com/platform/v1/connections/<connection_id> \
  -H 'Authorization: Bearer pk_live_…' \
  -H 'x-user-email: [email protected]'

# 5. Vend a token (backend only; never cache — responses are Cache-Control: no-store)
curl -X POST https://connect.pabbly.com/platform/v1/connections/<connection_id>/token \
  -H 'Authorization: Bearer pk_live_…' \
  -H 'x-user-email: [email protected]'
```

### Connection status

```
pending  →  active     (user authorized)
         →  rejected   (user declined)
         →  expired    (link lapsed before completion)
```

Poll the get-connection endpoint roughly **every 2 seconds**, bounded by the link's
`expires_at`.

### Token response — two shapes

Discriminate on **`auth_type`**:

```jsonc
// OAuth apps
{ "access_token": "ya29…", "token_type": "Bearer", "expires_in": 3599, "auth_type": "o_auth" }
// expires_in may be null if the provider doesn't expose an expiry.

// API-key / basic / header apps
{ "auth_type": "api_key", "fields": { /* the credential values */ }, "expires_in": null }
```

---

## 9. The frontend popup (browser helper)

The end user authorizes in a popup window. Your frontend only ever handles the
`request_link` — **never** the tenant key.

```html
<script src="pabbly-connect.js"></script> <!-- the browser helper -->
<script>
  PabblyConnect.open({
    requestLink:  data.request_link,
    requestToken: data.request_token,
    connectionId: data.connection_id,
    onSuccess: (connectionId) => notifyYourBackend(connectionId),
    onExit:    (reason) => showRetry(reason), // 'rejected' | 'expired' | 'closed' | 'timeout'
  });
</script>
```

The helper opens the hosted Connect page and polls a **public, token-gated** status
endpoint — the `request_token` gates it, so IDs alone reveal nothing and **no
credentials reach the browser**.

**Backend alternative:** instead of waiting in the browser, your backend can call
`await pc.waitForConnection(email, connectionId)`, which resolves when the status
leaves `pending`.

---

## 10. Agent / MCP access (optional)

For AI agents, the same capabilities are exposed as an **MCP server** (stateless,
streamable HTTP):

```
POST https://connect.pabbly.com/platform/v1/mcp
Authorization: Bearer pk_live_…
x-user-email: [email protected]        # required on the header endpoint
```

**Tools:** `search_apps`, `list_connections`, `create_connection_link`,
`check_connection_status`.

There is deliberately **no token tool** — agents *set up* connections; your backend
*vends* the tokens over REST (§6/§8). Tokens never enter an LLM context.

### Header-less clients (Claude Desktop, Cursor, …)

For clients that can't send custom headers, the **Settings → Platform API Keys** page
issues a **single-URL** variant — the entire client config is one URL:

```
POST https://connect.pabbly.com/platform/v1/mcp/<mcp_url_token>
```

With no email header, the connection is owned by the **key's own account** (personal
use).

---

## 11. Security & visibility model

Read this before going live.

- **Tenant-scoped, always.** You can list, read, vend, or delete **only the
  connections your own tenant created**. You can *never* enumerate a user's
  pre-existing connections, or any other tenant's — even with the correct email.
- **You assert the customer's identity.** Passing `x-user-email` is a claim made from
  *your* authenticated session. Passing an arbitrary email can at most create a *new*
  connection that the named user would still have to authorize via the hosted link —
  it cannot read anyone's existing data. **Always derive the email from your trusted
  session, never from raw user input.**
- **Secrets stay backend-side.** The tenant key and vended tokens must never reach a
  browser, mobile app, or LLM. Token responses are `no-store` and must not be
  persisted or logged.
- **Identity convergence.** A customer who has never used Connect is provisioned by
  email. When they later sign in to Connect with that same email, they inherit the
  same account — and the connections you made for them.

---

## 12. Errors & rate limits

Build your retry logic and UX on these **stable error codes** (`data.code`):

| HTTP | `data.code` | Meaning / action |
|:----:|-------------|------------------|
| 400 | `invalid_user_email` | Missing/invalid `x-user-email`. |
| 401 | `needs_reconnection` | Grant revoked/expired → re-run the connect flow. |
| 403 | `capability_disabled` | Your key lacks that capability. |
| 404 | `connection_deleted` | Unknown connection (or not yours). |
| 409 | `connection_in_use` | Delete blocked: a workflow still uses it. |
| 429 | `rate_limited` | Honor `Retry-After`. |
| 503 | `upstream_unavailable` | Transient provider error → retry with backoff. |

### Rate budgets (per tenant, per minute — tunable)

| Group | Default |
|-------|:-------:|
| Token endpoint | 600 / min |
| Everything else (apps, connections, MCP) | 120 / min |

`429` responses carry `Retry-After` (in seconds). The Node SDK surfaces this as
`RateLimitedError.retryAfter`.

---

## 13. Production checklist

- [ ] Tenant key stored in your backend secret manager — **never** shipped to a client.
- [ ] `x-user-email` always sourced from your authenticated session.
- [ ] Token responses used immediately — never persisted, logged, or shown to users/LLMs.
- [ ] `needs_reconnection` (401) drives a reconnect UX.
- [ ] `429` honored via `Retry-After`; `503` retried with backoff.
- [ ] Key-rotation runbook understood: **rotate → deploy new key → promote**.

---

## 14. FAQ

**Do I need separate keys per customer?**
No. One tenant key serves all your customers; you scope each call with `x-user-email`.

**What happens if I call `getToken` and the OAuth grant expired?**
You get `401 needs_reconnection` (`NeedsReconnectionError` in the SDK). Send the user
back through the connect flow (§5) to create a fresh connection.

**Can I cache vended tokens?**
No. Token responses are `Cache-Control: no-store`. Call `getToken` each time you need
to act — Pabbly refreshes server-side on demand and returns a currently-valid token.

**Can the frontend ever see my tenant key?**
No. The frontend only ever receives the `request_link` / `request_token` /
`connection_id`. All tenant-key calls happen on your backend.

**What auth types are supported beyond OAuth?**
API-key, basic, and header-based apps. For these, `getToken` returns
`{ auth_type, fields }` instead of an `access_token` — read the credential values from
`fields`.

---

*Questions? Reply in this thread and the Pabbly team will help you get integrated.*
 
Top