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.
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.
1PAYCUX_API_KEY=sk_example_7f4c1b9ad2e84c6f2PAYCUX_CLIENT_ID=client_01HQZX8N4T3PAYCUX_REDIRECT_URI=http://localhost:3000/callback/sso
Then, in the dashboard:
- 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.
- 2Add a connection to that organization and pick the provider type: SAML or OIDC.
- 3Either paste the metadata the customer's IT team gives you, or send them an Admin Portal link and let them do it themselves.
- 4Add the redirect URI to the allowed list. A mismatch here is the single most common cause of a failed first sign-in.
- 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.
1import { Paycux } from "@paycux/node";23const paycux = new Paycux(process.env.PAYCUX_API_KEY);45export async function POST(request: Request) {6 const { email } = await request.json();7 const domain = email.split("@")[1];89 const { data } = await paycux.organizations.list({ domains: [domain] });10 const organization = data[0];1112 if (!organization) {13 return Response.json({ sso: false });14 }1516 const url = paycux.sso.getAuthorizationUrl({17 clientId: process.env.PAYCUX_CLIENT_ID,18 redirectUri: process.env.PAYCUX_REDIRECT_URI,19 organizationId: organization.id,20 });2122 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.
1import { Paycux } from "@paycux/node";23const paycux = new Paycux(process.env.PAYCUX_API_KEY);45export 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");910 if (error || !code) {11 return Response.redirect("/sign-in?error=sso_failed");12 }1314 const { profile } = await paycux.sso.getProfileAndToken({15 clientId: process.env.PAYCUX_CLIENT_ID,16 code,17 });1819 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 });2728 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.
1import { Paycux } from "@paycux/node";23const paycux = new Paycux(process.env.PAYCUX_API_KEY);45export async function POST(request: Request) {6 const payload = await request.text();7 const signature = request.headers.get("paycux-signature");89 const event = paycux.webhooks.verify({10 payload,11 signature,12 secret: process.env.PAYCUX_WEBHOOK_SECRET,13 });1415 if (event.event === "connection.activated") {16 await markTenantSsoReady(event.data.organizationId);17 }1819 if (event.event === "connection.deactivated") {20 await alertSupport(event.data.organizationId, "SSO connection deactivated");21 }2223 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.
- 1Create a test organization with a domain you control and connect a provider tenant you own.
- 2Sign in through the connection and confirm the callback received an email, a first and last name, and a stable idpId.
- 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.
- 4Deactivate the connection and confirm your app falls back to ordinary sign-in rather than erroring.
- 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.
| Method | Path | What it does |
|---|---|---|
| GET | /sso/authorize | Start a sign-in for an organization or a specific connection. |
| POST | /sso/token | Exchange an authorization code for a profile and access token. |
| GET | /sso/profile | Fetch the normalised profile for an access token. |
| GET | /connections | List connections, filtered by organization or state. |
| GET | /connections/:id | Read one connection with its provider type and status. |
| DELETE | /connections/:id | Remove a connection and stop accepting its assertions. |
| GET | /organizations | List organizations, including a lookup by email domain. |
| POST | /organizations | Create an organization and claim its domains. |