Multiple connections
Let a user or an organization connect more than one account for the same Pipes provider.
Multiple connections let the same owner connect several accounts for one provider: a personal and a work GitHub account, or one API key per project. “Owner” means the same connection owner as everywhere else in Pipes: a user, or an organization for shared connections. Each owner’s connections to a provider are counted and selected separately.
An integration built for multiple connections follows three rules:
- Create with
connection_intent: "add". - Read with
supports_multiple_connections: true. - Act on a connection by passing its
connected_account_id.
Store the connected_account_id from each new connection against your own
record of what it is for. It is the only handle for a connection.
Integrations that already use Pipes keep their current behavior until they adopt these rules. See existing integrations for how requests without them behave and how to migrate.
Creating an additional connection is unavailable until WorkOS enables it for
your team. Until then, an add creates a connection only when the owner
has none for the provider, and otherwise returns
404 multiple_connections_unavailable. Requests without connection_intent,
and reauthorization, are unaffected.
Two fields describe a connection but never identify it:
account_display_nameis a label for people, such as “Work GitHub”. It is mutable and up to 255 characters, and several connections can share one. When a write sets a label that another connection for the same owner and provider already uses, the response includes awarningsentry with the codeduplicate_account_display_name; the write still succeeds.account_identifierdescribes which provider account a connection points at. It never identifies the connection; that is alwaysconnected_account_id. WorkOS does not infer it from a label or credential: it holds only what your application supplies.
WorkOS does not expose a developer-declared uniqueness field, and it neither refuses nor detects duplicate provider accounts. Two deliberate adds can connect the same provider account twice.
Send connection_intent: "add" on any creation request. The owner gains
another connection instead of reusing or replacing an existing one.
Request an authorization URL with connection_intent: "add" and send the user
through it. You can set the label up front with account_display_name.
When the user completes authorization, WorkOS creates the new connection. If
the add fails, the user returns to your return_to URL with an error query
parameter (or sees an error page when you set none), and a
pipes.account_connection.add_failed event reports it.
Use the creation endpoints for
API keys and
client credentials.
They accept only connection_intent: "add" and never update an existing
connection.
To rotate a key or credentials later, use the PUT endpoints for
API keys and client
credentials with the connection’s connected_account_id. Sending add to a
PUT endpoint returns 400.
The token import endpoints for
users and
organizations
accept connection_intent: "add" in the same way.
An add is refused once the owner has 25 connections for the provider, with
409 connected_account_limit_reached. Disconnect a connection before adding
another.
Retrying an add whose response was lost can create a second connection. To make
a synchronous add retry-safe, send an Idempotency-Key header on the REST
creation endpoints for API keys, client credentials, and imported tokens, or
the idempotencyKey input on the equivalent Widgets API mutations. Reuse the
same key for every retry of one request.
- A matching retry within 24 hours returns the connection the first attempt created, unchanged, even if the retry carries different inputs.
- Reusing the key for a different owner, provider, or authentication method,
or after 24 hours while that connection exists, returns
409 idempotency_key_conflict. - Once that connection is deleted, the key creates a new connection.
- A REST add and a Widgets API add of the same kind, for the same owner and provider, are one operation and share the key.
The key is optional; without it, retries can create duplicates. It is not lifetime duplicate prevention. OAuth adds take no key: each authorization completes once, and starting a second authorization creates a second connection.
Some WorkOS SDKs send an Idempotency-Key automatically when they retry a
POST. If you write your own retries, generate one key per add and send it on
every attempt.
Send supports_multiple_connections=true when
listing providers to receive every connection
for the owner in each provider’s connected_accounts array.
A request that declares supports_multiple_connections: true but names no
connection, while the owner has several, returns 409 rather than guessing.
Ask the user which connection to use, then repeat the request with its
connected_account_id:
A malformed connected_account_id returns 400. An id that does not exist
and an id that belongs to another owner or provider both return the same
404, so a failed selector never creates, changes, vends, or reveals another
connection.
Pass connected_account_id when you
fetch an access token or
vend credentials:
With Relay, name the connection with the
X-Relay-Connected-Account header. Relay removes the header before forwarding
the request.
- Rename or update: pass
connected_account_idto update a connected account, including itsaccount_display_name. - Reauthorize: request an authorization URL with
connection_intent: "reauthorize"and theconnected_account_idto reauthorize. The new grant replaces that connection’s credentials. - Delete: pass
connected_account_idto delete a connected account to remove exactly that connection.
Subscribe to the pipes.account_connection.* events, which cover every
connection: connected, disconnected, reauthorization_needed,
connection_failed, and add_failed. Each payload’s id is the connection’s
connected_account_id, so match events to your records by it.
pipes.account_connection.add_failed reports a failed add. It can occur before
any connection exists, so it carries no connected account id. See
Pipes events for payloads.
The Pipes and Pipes Admin widgets
support multiple connections in their sectioned view, which you turn on with
the enableSharedConnections prop. This requires @workos-inc/widgets 1.20.0
or later. The flat list rendered without that prop shows one connection per
provider.
- One connection. The provider row looks as it did before. Its menu adds Rename and Connect another account.
- Several connections. The provider row shows how many there are, and each connection gets its own row below it, oldest first, with its own status and actions. A row shows the connection’s name, falling back to its account identifier and then the provider name.
- Adding. Add connection and Connect another account appear only when
the owner can add one, so they stay hidden while multiple connections are
unavailable and at the connection limit. In Pipes, adding asks for an
optional name; Pipes Admin doesn’t, so admins name a connection with
Rename afterwards. API key and client credentials adds send an
idempotencyKey, so retrying after an ambiguous failure returns the same connection. - Acting on a connection. Reauthorizing, updating a key or credentials, and disconnecting act on exactly that row’s connection.
- Renaming. A name another connection for the same provider already uses still saves, with a notice suggesting a distinct one.
The Shared connections section that members see still shows one row per provider, even when an organization shares several connections.
| Status | Code | Meaning |
|---|---|---|
| 400 | An invalid combination: add with connected_account_id, reauthorize without it, add on a PUT endpoint, or a malformed selector. |
|
| 404 | multiple_connections_unavailable |
The add would create an additional connection before WorkOS has enabled them for your team. |
| 404 | The connected_account_id does not exist or belongs to another owner or provider. |
|
| 404 | connected_account_not_found |
The connection named for reauthorization does not exist for this owner and provider. |
| 409 | account_selection_required |
The request declared supports_multiple_connections, named no connection, and several match. |
| 409 | connected_account_limit_reached |
The owner already has 25 connections for this provider. |
| 409 | idempotency_key_conflict |
The Idempotency-Key belongs to a different add, or its 24-hour replay window has passed. |
An OAuth reauthorization whose connection is deleted before the user returns
sends the user to your return_to URL with error=target_deleted. No webhook
event is emitted, since the connection no longer exists.
Pipes integrations built before multiple connections send no
connection_intent, no supports_multiple_connections, and often no
connected_account_id. Those requests keep working permanently. To keep them
predictable once an owner has several connections, every connection carries a
connection_role:
compatibility: the connection that requests without these fields use. Each owner has at most one per provider.standard: any further connection for the same owner and provider. Only requests that name it withconnected_account_id, or that declaresupports_multiple_connections: true, can see it.
The first connection for an owner becomes the compatibility connection, even
when it is created with connection_intent: "add"; later adds create standard
connections. An integration that follows the three rules above never needs to
check the role. It does not mean preferred, primary, or oldest, and it never
changes.
- Reads without
supports_multiple_connectionsreturn only the compatibility connection, and are never refused withaccount_selection_required. In a plural provider list, the singularconnected_accountfield is the compatibility connection. - Token fetches, credential vending, and Relay without a selector use the compatibility connection.
PUTwrites withoutconnected_account_idupdate the compatibility connection.- Deletes without
connected_account_iddelete the compatibility connection. - Creation without
connection_intentonly ever addresses the compatibility connection: it fills an empty slot, and otherwise OAuth andPUTupdate the existing compatibility connection while token imports return409. It never creates a standard connection.
Deleting the compatibility connection leaves the slot empty and never promotes
a standard connection into it. The owner’s standard connections then stay
invisible to requests without the new fields. While any standard connection
remains, an add creates another standard connection; only a request without
connection_intent refills the slot. The 25-connection limit never blocks that
refill.
Pipes emits two families of connection events:
| Family | Covers | Events |
|---|---|---|
Compatibility pipes.connected_account.* |
Compatibility connections only | connected, disconnected, reauthorization_needed, connection_failed |
All-connections pipes.account_connection.* |
Compatibility and standard connections | connected, disconnected, reauthorization_needed, connection_failed, add_failed |
An endpoint subscribed to both receives two events for each compatibility connection change: two views of the same change, not two connections.
- Store connection ids. Save
connected_account_idagainst your own records, and stop assuming one connection per owner and provider. - Read in plural. Send
supports_multiple_connections: trueand handle409 account_selection_required. - Name the connection you use. Pass
connected_account_idwhen fetching tokens, vending credentials, calling Relay, updating, and deleting. - Add explicitly. Send
connection_intent: "add"when the user connects another account, with anIdempotency-Keyon synchronous adds. - Move webhook handlers to the
pipes.account_connection.*family, and unsubscribe frompipes.connected_account.*.
Move every client that reads connections at the same time. A backend job or
older widget that still reads without supports_multiple_connections keeps
working on the compatibility connection, which may not be the one a user meant
once they have several.