# 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.*