Skip to content

Integrations

Integrate Enterprise SSO

Enterprise SSO turns every identity provider your customers run into the same three calls: build an authorization URL, exchange the code, read the profile. You write that once. Each new customer becomes a connection record rather than a sprint.

Before you begin

  • A working sign-in flow. SSO is an additional path into your app, not a replacement for the one you already have.
  • An organization model. A connection belongs to an organization, so you need somewhere to record which tenant a person signed in to.
  • A way to route people to the right connection: an email domain, a workspace subdomain, or an explicit organization picker.
  • For live customers, an IT admin on the other side who can create the application in their identity provider. The Admin Portal removes almost all of that work.

Install

Every example on this page uses the Node.js client. The same calls exist in the Python, Go, Ruby, PHP and Java SDKs, following the naming conventions of each language.

terminal
1npm i @paycux/node

Configure

SSO uses the same client ID and API key as the rest of the platform. The only extra value is the redirect URI, which must match exactly what the identity provider will be told to send people back to.

.env.local
1PAYCUX_API_KEY=sk_example_7f4c1b9ad2e84c6f
2PAYCUX_CLIENT_ID=client_01HQZX8N4T
3PAYCUX_REDIRECT_URI=http://localhost:3000/callback/sso

Then, in the dashboard:

  1. 1Create an organization for the customer and add the email domains they own. Domain ownership is what lets you route a sign-in without asking which company someone works for.
  2. 2Add a connection to that organization and pick the provider type: SAML or OIDC.
  3. 3Either paste the metadata the customer's IT team gives you, or send them an Admin Portal link and let them do it themselves.
  4. 4Add the redirect URI to the allowed list. A mismatch here is the single most common cause of a failed first sign-in.
  5. 5Leave the connection inactive until a test sign-in succeeds, then activate it.

Implement

Your application never speaks SAML or OIDC. It sends people to an authorization URL scoped to an organization or a connection, and receives a normalised profile on the way back. The differences between providers are absorbed before your callback runs.

Route a sign-in to the right connection

Look up the organization from the email domain the person typed, then build the authorization URL for it. If no organization matches, fall back to your ordinary sign-in.

app/api/sso/start/route.ts
1import { Paycux } from "@paycux/node";
2
3const paycux = new Paycux(process.env.PAYCUX_API_KEY);
4
5export async function POST(request: Request) {
6 const { email } = await request.json();
7 const domain = email.split("@")[1];
8
9 const { data } = await paycux.organizations.list({ domains: [domain] });
10 const organization = data[0];
11
12 if (!organization) {
13 return Response.json({ sso: false });
14 }
15
16 const url = paycux.sso.getAuthorizationUrl({
17 clientId: process.env.PAYCUX_CLIENT_ID,
18 redirectUri: process.env.PAYCUX_REDIRECT_URI,
19 organizationId: organization.id,
20 });
21
22 return Response.json({ sso: true, url });
23}

Exchange the code for a profile

The profile is the same shape whether it arrived over SAML or OIDC. Use the connection and organization IDs on it to decide which tenant the session belongs to, and never trust an organization ID sent by the browser.

app/callback/sso/route.ts
1import { Paycux } from "@paycux/node";
2
3const paycux = new Paycux(process.env.PAYCUX_API_KEY);
4
5export async function GET(request: Request) {
6 const params = new URL(request.url).searchParams;
7 const code = params.get("code");
8 const error = params.get("error");
9
10 if (error || !code) {
11 return Response.redirect("/sign-in?error=sso_failed");
12 }
13
14 const { profile } = await paycux.sso.getProfileAndToken({
15 clientId: process.env.PAYCUX_CLIENT_ID,
16 code,
17 });
18
19 await upsertUser({
20 externalId: profile.idpId,
21 email: profile.email,
22 firstName: profile.firstName,
23 lastName: profile.lastName,
24 connectionId: profile.connectionId,
25 organizationId: profile.organizationId,
26 });
27
28 return Response.redirect("/");
29}

React to connection changes

An IT admin can rotate a signing certificate or repoint a connection at any time. Subscribe to the connection events so your support team hears about it before your customer does.

app/api/webhooks/route.ts
1import { Paycux } from "@paycux/node";
2
3const paycux = new Paycux(process.env.PAYCUX_API_KEY);
4
5export async function POST(request: Request) {
6 const payload = await request.text();
7 const signature = request.headers.get("paycux-signature");
8
9 const event = paycux.webhooks.verify({
10 payload,
11 signature,
12 secret: process.env.PAYCUX_WEBHOOK_SECRET,
13 });
14
15 if (event.event === "connection.activated") {
16 await markTenantSsoReady(event.data.organizationId);
17 }
18
19 if (event.event === "connection.deactivated") {
20 await alertSupport(event.data.organizationId, "SSO connection deactivated");
21 }
22
23 return new Response("ok", { status: 200 });
24}

Verify

Test with a real identity provider before you hand the flow to a customer. A provider that returns an assertion your app cannot read is far more common than one that refuses outright.

  1. 1Create a test organization with a domain you control and connect a provider tenant you own.
  2. 2Sign in through the connection and confirm the callback received an email, a first and last name, and a stable idpId.
  3. 3Sign in a second time and confirm the idpId is unchanged. If it moves between sign-ins, the provider is sending a transient identifier and the name ID format needs correcting.
  4. 4Deactivate the connection and confirm your app falls back to ordinary sign-in rather than erroring.
  5. 5Check the connection's recent sign-ins in the dashboard, where a failed assertion shows the reason it was rejected.

Reference

Request and response shapes for each of these live in the API reference. Paths are relative to https://api.paycux.com.

MethodPathWhat it does
GET/sso/authorizeStart a sign-in for an organization or a specific connection.
POST/sso/tokenExchange an authorization code for a profile and access token.
GET/sso/profileFetch the normalised profile for an access token.
GET/connectionsList connections, filtered by organization or state.
GET/connections/:idRead one connection with its provider type and status.
DELETE/connections/:idRemove a connection and stop accepting its assertions.
GET/organizationsList organizations, including a lookup by email domain.
POST/organizationsCreate an organization and claim its domains.

Next steps