Integrations
Integrate Directory Sync
Directory Sync keeps your user table in step with the systems your customers already run. Someone joins, changes team or leaves, and your application hears about it within seconds — over SCIM from an identity provider, or on a schedule from an HR system.
Before you begin
- A webhook endpoint that can accept a POST over HTTPS and respond quickly. Directory events are pushed, not polled.
- A stable external identifier on your user records. Email changes; the directory's own ID does not.
- A deprovisioning policy decided in advance. Deleting a user takes their data with them, so most products deactivate instead.
- A group model, if your customers expect group membership to map onto roles or permissions in your product.
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
The webhook secret is the important value here. It is what proves a delivery came from Paycux rather than from anyone who found your endpoint, and it is per environment.
1PAYCUX_API_KEY=sk_example_7f4c1b9ad2e84c6f2PAYCUX_WEBHOOK_SECRET=whsec_example_2b6d9f313PAYCUX_DIRECTORY_WEBHOOK_URL=https://yourapp.example/api/webhooks/directory
Then, in the dashboard:
- 1Create the organization for the customer if it does not exist yet, then add a directory to it.
- 2Choose the source: SCIM 2.0 for an identity provider, or a supported HR system for an employee record of truth.
- 3For SCIM, copy the endpoint URL and bearer token and hand them to the customer's IT admin — or send an Admin Portal link and let them collect their own.
- 4Register your webhook URL and copy the signing secret. Store it as a secret, not in the repository.
- 5Subscribe to the user and group events you intend to handle, and leave the rest off so your endpoint is not woken for nothing.
Implement
Almost all of the work is one webhook handler. Verify, deduplicate, then apply. Treat every event as a statement about the desired end state rather than as an instruction, and out-of-order deliveries stop being a problem.
Handle directory events
Verify the signature before parsing anything you trust. Directory events carry the full current record, so an upsert keyed on the directory user ID is both simpler and safer than applying a diff.
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 let event;10 try {11 event = paycux.webhooks.verify({12 payload,13 signature,14 secret: process.env.PAYCUX_WEBHOOK_SECRET,15 });16 } catch {17 return new Response("invalid signature", { status: 401 });18 }1920 if (await alreadyProcessed(event.id)) {21 return new Response("ok", { status: 200 });22 }2324 switch (event.event) {25 case "dsync.user.created":26 case "dsync.user.updated":27 await upsertUser({28 directoryUserId: event.data.id,29 organizationId: event.data.organizationId,30 email: event.data.emails.find((e) => e.primary)?.value,31 firstName: event.data.firstName,32 lastName: event.data.lastName,33 active: event.data.state === "active",34 });35 break;3637 case "dsync.user.deleted":38 await deactivateUser(event.data.id);39 break;4041 case "dsync.group.user_added":42 await addToGroup(event.data.user.id, event.data.group.id);43 break;4445 case "dsync.group.user_removed":46 await removeFromGroup(event.data.user.id, event.data.group.id);47 break;48 }4950 await markProcessed(event.id);51 return new Response("ok", { status: 200 });52}
Backfill an existing directory
Events only describe what happens from now on. When a customer connects a directory that already has two thousand people in it, read the current state once and page through it.
1import { Paycux } from "@paycux/node";23const paycux = new Paycux(process.env.PAYCUX_API_KEY);45export async function backfill(directoryId: string) {6 let after: string | undefined;78 do {9 const page = await paycux.directorySync.listUsers({10 directory: directoryId,11 limit: 100,12 after,13 });1415 for (const user of page.data) {16 await upsertUser({17 directoryUserId: user.id,18 organizationId: user.organizationId,19 email: user.emails.find((e) => e.primary)?.value,20 firstName: user.firstName,21 lastName: user.lastName,22 active: user.state === "active",23 });24 }2526 after = page.listMetadata.after;27 } while (after);28}
Map groups onto roles
Customers usually want their existing groups to mean something in your product. Read the group list, let an admin pair each one with a role, and store the mapping alongside the organization.
1import { Paycux } from "@paycux/node";23const paycux = new Paycux(process.env.PAYCUX_API_KEY);45export async function GET(request: Request) {6 const directory = new URL(request.url).searchParams.get("directory");78 const groups = await paycux.directorySync.listGroups({ directory, limit: 100 });910 return Response.json(11 groups.data.map((g) => ({ id: g.id, name: g.name, members: g.memberCount }))12 );13}
Verify
The failure mode that matters is the quiet one: a person leaves the company and keeps their session. Test the removal path first.
- 1Connect a test directory you control and confirm the initial sync reports the number of users you expect.
- 2Create a user in the directory and confirm your application receives dsync.user.created within a few seconds.
- 3Suspend that user in the directory and confirm your record is deactivated and their session no longer works.
- 4Add the user to a group and confirm the membership event arrives and maps to the role you configured.
- 5Deliberately return a 500 from your endpoint once, then confirm the delivery is retried and your handler does not apply it twice.
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 | /directories | List connected directories with type and sync state. |
| GET | /directories/:id | Read one directory, including its last successful sync. |
| DELETE | /directories/:id | Disconnect a directory and stop further events. |
| GET | /directory_users | Page through the users in a directory. |
| GET | /directory_users/:id | Read one directory user with raw provider attributes. |
| GET | /directory_groups | List groups and their member counts. |
| GET | /directory_groups/:id | Read one group and its members. |
| POST | /directories/:id/sync | Trigger a full resynchronisation on demand. |