Help 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.
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:
You never see or store the user's third-party password, and you never run the OAuth refresh machinery yourself.
| 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. |
A key is issued in one of two ways:
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.
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)
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-emailis 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 returns400 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.
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.
The Node SDK is the fastest path. Requires Node ≥ 18.
npm install @pabbly/connect-platform
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;
}
}
Construct once, reuse everywhere:
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,
});
| 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
listApps/getAppdo not.
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 |
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 |
# 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]'
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.
Discriminate on auth_type:
// 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 }
The end user authorizes in a popup window. Your frontend only ever handles the
request_link — never the tenant key.
<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.
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.
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).
Read this before going live.
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.no-store and must not be
persisted or logged.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. |
| 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.
x-user-email always sourced from your authenticated session.needs_reconnection (401) drives a reconnect UX.429 honored via Retry-After; 503 retried with backoff.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.