Skip to content

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.

terminal
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.

.env.local
1PAYCUX_API_KEY=sk_example_7f4c1b9ad2e84c6f
2PAYCUX_WEBHOOK_SECRET=whsec_example_2b6d9f31
3PAYCUX_DIRECTORY_WEBHOOK_URL=https://yourapp.example/api/webhooks/directory

Then, in the dashboard:

  1. 1Create the organization for the customer if it does not exist yet, then add a directory to it.
  2. 2Choose the source: SCIM 2.0 for an identity provider, or a supported HR system for an employee record of truth.
  3. 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.
  4. 4Register your webhook URL and copy the signing secret. Store it as a secret, not in the repository.
  5. 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.

app/api/webhooks/directory/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 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 }
19
20 if (await alreadyProcessed(event.id)) {
21 return new Response("ok", { status: 200 });
22 }
23
24 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;
36
37 case "dsync.user.deleted":
38 await deactivateUser(event.data.id);
39 break;
40
41 case "dsync.group.user_added":
42 await addToGroup(event.data.user.id, event.data.group.id);
43 break;
44
45 case "dsync.group.user_removed":
46 await removeFromGroup(event.data.user.id, event.data.group.id);
47 break;
48 }
49
50 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.

scripts/backfill.ts
1import { Paycux } from "@paycux/node";
2
3const paycux = new Paycux(process.env.PAYCUX_API_KEY);
4
5export async function backfill(directoryId: string) {
6 let after: string | undefined;
7
8 do {
9 const page = await paycux.directorySync.listUsers({
10 directory: directoryId,
11 limit: 100,
12 after,
13 });
14
15 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 }
25
26 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.

app/api/directory/groups/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 directory = new URL(request.url).searchParams.get("directory");
7
8 const groups = await paycux.directorySync.listGroups({ directory, limit: 100 });
9
10 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.

  1. 1Connect a test directory you control and confirm the initial sync reports the number of users you expect.
  2. 2Create a user in the directory and confirm your application receives dsync.user.created within a few seconds.
  3. 3Suspend that user in the directory and confirm your record is deactivated and their session no longer works.
  4. 4Add the user to a group and confirm the membership event arrives and maps to the role you configured.
  5. 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.

MethodPathWhat it does
GET/directoriesList connected directories with type and sync state.
GET/directories/:idRead one directory, including its last successful sync.
DELETE/directories/:idDisconnect a directory and stop further events.
GET/directory_usersPage through the users in a directory.
GET/directory_users/:idRead one directory user with raw provider attributes.
GET/directory_groupsList groups and their member counts.
GET/directory_groups/:idRead one group and its members.
POST/directories/:id/syncTrigger a full resynchronisation on demand.

Next steps