Skip to content

Commit 5237aa2

Browse files
feat: Add IT Contacts API (#1681)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 203f845 commit 5237aa2

10 files changed

Lines changed: 351 additions & 1 deletion
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"object": "it_contact",
3+
"id": "it_contact_01HXYZ123456789ABCDEFGHIJ",
4+
"email": "it-contact@example.com",
5+
"created_at": "2026-01-15T12:00:00.000Z",
6+
"updated_at": "2026-01-15T12:00:00.000Z"
7+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"object": "list",
3+
"data": [
4+
{
5+
"object": "it_contact",
6+
"id": "it_contact_01HXYZ123456789ABCDEFGHIJ",
7+
"email": "it-contact@example.com",
8+
"created_at": "2026-01-15T12:00:00.000Z",
9+
"updated_at": "2026-01-15T12:00:00.000Z"
10+
}
11+
],
12+
"list_metadata": {
13+
"before": null,
14+
"after": null
15+
}
16+
}

src/organizations/interfaces/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export * from './create-organization-options.interface';
22
export * from './domain-data.interface';
3+
export * from './it-contact-options.interface';
4+
export * from './it-contact.interface';
35
export * from './list-organization-feature-flags-options.interface';
46
export * from './list-organizations-options.interface';
57
export * from './organization.interface';
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
export const ItContactIntent = {
2+
SSO: 'sso',
3+
DirectorySync: 'directory_sync',
4+
LogStreams: 'log_streams',
5+
DomainVerification: 'domain_verification',
6+
BringYourOwnKey: 'bring_your_own_key',
7+
} as const;
8+
9+
export type ItContactIntent =
10+
(typeof ItContactIntent)[keyof typeof ItContactIntent];
11+
12+
export interface ListItContactsOptions {
13+
/** Unique identifier of the Organization. */
14+
organizationId: string;
15+
}
16+
17+
export interface CreateItContactOptions {
18+
/** Unique identifier of the Organization. */
19+
organizationId: string;
20+
/** The email address of the IT Contact. */
21+
email: string;
22+
}
23+
24+
export interface SerializedCreateItContactOptions {
25+
email: string;
26+
}
27+
28+
export interface DeleteItContactOptions {
29+
/** Unique identifier of the Organization. */
30+
organizationId: string;
31+
/** Unique identifier of the IT Contact. */
32+
contactId: string;
33+
}
34+
35+
export interface InviteItContactOptions {
36+
/** Unique identifier of the Organization. */
37+
organizationId: string;
38+
/** Unique identifier of the IT Contact. */
39+
contactId: string;
40+
/** The Admin Portal features that the IT Contact can configure. */
41+
intents: ItContactIntent[];
42+
}
43+
44+
export interface SerializedInviteItContactOptions {
45+
intents: ItContactIntent[];
46+
}
47+
48+
export interface RevokeItContactOptions {
49+
/** Unique identifier of the Organization. */
50+
organizationId: string;
51+
/** Unique identifier of the IT Contact. */
52+
contactId: string;
53+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export interface ItContact {
2+
/** Distinguishes the IT Contact object. */
3+
object: 'it_contact';
4+
/** Unique identifier of the IT Contact. */
5+
id: string;
6+
/** The email address of the IT Contact. */
7+
email: string;
8+
/** An ISO 8601 timestamp. */
9+
createdAt: string;
10+
/** An ISO 8601 timestamp. */
11+
updatedAt: string;
12+
}
13+
14+
export interface ItContactResponse {
15+
object: 'it_contact';
16+
id: string;
17+
email: string;
18+
created_at: string;
19+
updated_at: string;
20+
}

src/organizations/organizations.spec.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ import {
55
fetchSearchParams,
66
fetchHeaders,
77
fetchBody,
8+
fetchMethod,
89
} from '../common/utils/test-utils';
910
import { WorkOS } from '../workos';
1011
import clearStripeCustomerId from './fixtures/clear-stripe-customer-id.json';
1112
import createOrganizationInvalid from './fixtures/create-organization-invalid.json';
1213
import createOrganization from './fixtures/create-organization.json';
1314
import getOrganization from './fixtures/get-organization.json';
15+
import itContact from './fixtures/it-contact.json';
16+
import listItContacts from './fixtures/list-it-contacts.json';
1417
import listOrganizationsFixture from './fixtures/list-organizations.json';
1518
import updateOrganization from './fixtures/update-organization.json';
1619
import setStripeCustomerId from './fixtures/set-stripe-customer-id.json';
1720
import setStripeCustomerIdDisabled from './fixtures/set-stripe-customer-id-disabled.json';
18-
import { DomainDataState } from './interfaces';
21+
import { DomainDataState, ItContactIntent } from './interfaces';
1922

2023
const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');
2124

@@ -363,4 +366,102 @@ describe('Organizations', () => {
363366
});
364367
});
365368
});
369+
370+
describe('IT Contacts', () => {
371+
describe('listItContacts', () => {
372+
it('returns the organization’s IT contacts', async () => {
373+
fetchOnce(listItContacts);
374+
375+
const { data, listMetadata } =
376+
await workos.organizations.listItContacts({
377+
organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T',
378+
});
379+
380+
expect(fetchMethod()).toBe('GET');
381+
expect(fetchURL()).toContain(
382+
'/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts',
383+
);
384+
385+
expect(data).toEqual([
386+
{
387+
object: 'it_contact',
388+
id: 'it_contact_01HXYZ123456789ABCDEFGHIJ',
389+
email: 'it-contact@example.com',
390+
createdAt: '2026-01-15T12:00:00.000Z',
391+
updatedAt: '2026-01-15T12:00:00.000Z',
392+
},
393+
]);
394+
expect(listMetadata).toEqual({ before: null, after: null });
395+
});
396+
});
397+
398+
describe('createItContact', () => {
399+
it('creates an IT contact', async () => {
400+
fetchOnce(itContact);
401+
402+
const subject = await workos.organizations.createItContact({
403+
organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T',
404+
email: 'it-contact@example.com',
405+
});
406+
407+
expect(fetchMethod()).toBe('POST');
408+
expect(fetchURL()).toContain(
409+
'/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts',
410+
);
411+
expect(fetchBody()).toEqual({ email: 'it-contact@example.com' });
412+
413+
expect(subject.id).toBe('it_contact_01HXYZ123456789ABCDEFGHIJ');
414+
});
415+
});
416+
417+
describe('deleteItContact', () => {
418+
it('deletes an IT contact', async () => {
419+
fetchOnce({}, { status: 204 });
420+
421+
await workos.organizations.deleteItContact({
422+
organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T',
423+
contactId: 'it_contact_01HXYZ123456789ABCDEFGHIJ',
424+
});
425+
426+
expect(fetchMethod()).toBe('DELETE');
427+
expect(fetchURL()).toContain(
428+
'/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts/it_contact_01HXYZ123456789ABCDEFGHIJ',
429+
);
430+
});
431+
});
432+
433+
describe('inviteItContact', () => {
434+
it('invites an IT contact', async () => {
435+
fetchOnce({}, { status: 204 });
436+
437+
await workos.organizations.inviteItContact({
438+
organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T',
439+
contactId: 'it_contact_01HXYZ123456789ABCDEFGHIJ',
440+
intents: [ItContactIntent.SSO, ItContactIntent.DirectorySync],
441+
});
442+
443+
expect(fetchMethod()).toBe('POST');
444+
expect(fetchURL()).toContain(
445+
'/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts/it_contact_01HXYZ123456789ABCDEFGHIJ/invite',
446+
);
447+
expect(fetchBody()).toEqual({ intents: ['sso', 'directory_sync'] });
448+
});
449+
});
450+
451+
describe('revokeItContact', () => {
452+
it('revokes an IT contact’s invitation', async () => {
453+
fetchOnce({}, { status: 204 });
454+
455+
await workos.organizations.revokeItContact({
456+
organizationId: 'org_01EHT88Z8J8795GZNQ4ZP1J81T',
457+
contactId: 'it_contact_01HXYZ123456789ABCDEFGHIJ',
458+
});
459+
460+
expect(fetchMethod()).toBe('POST');
461+
expect(fetchURL()).toContain(
462+
'/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/it_contacts/it_contact_01HXYZ123456789ABCDEFGHIJ/revoke',
463+
);
464+
});
465+
});
466+
});
366467
});

src/organizations/organizations.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
import { AutoPaginatable } from '../common/utils/pagination';
22
import { WorkOS } from '../workos';
3+
import { List, ListResponse } from '../common/interfaces';
34
import {
5+
CreateItContactOptions,
46
CreateOrganizationOptions,
57
CreateOrganizationRequestOptions,
8+
DeleteItContactOptions,
9+
InviteItContactOptions,
10+
ItContact,
11+
ItContactResponse,
12+
ListItContactsOptions,
613
ListOrganizationsOptions,
714
Organization,
815
OrganizationResponse,
16+
RevokeItContactOptions,
917
UpdateOrganizationOptions,
1018
} from './interfaces';
1119
import {
20+
deserializeItContact,
1221
deserializeOrganization,
22+
serializeCreateItContactOptions,
1323
serializeCreateOrganizationOptions,
24+
serializeInviteItContactOptions,
1425
serializeUpdateOrganizationOptions,
1526
} from './serializers';
1627

@@ -151,4 +162,113 @@ export class Organizations {
151162

152163
return deserializeOrganization(data);
153164
}
165+
166+
/**
167+
* List IT Contacts
168+
*
169+
* Get the IT Contacts for an Organization.
170+
* @param options - Object containing the Organization ID.
171+
* @returns {Promise<List<ItContact>>}
172+
* @throws {AuthorizationException} 403
173+
* @throws {NotFoundException} 404
174+
*/
175+
async listItContacts(
176+
options: ListItContactsOptions,
177+
): Promise<List<ItContact>> {
178+
const { organizationId } = options;
179+
180+
const { data } = await this.workos.get<ListResponse<ItContactResponse>>(
181+
`/organizations/${organizationId}/it_contacts`,
182+
);
183+
184+
return {
185+
object: data.object,
186+
data: data.data.map(deserializeItContact),
187+
listMetadata: {
188+
before: data.list_metadata.before,
189+
after: data.list_metadata.after,
190+
},
191+
};
192+
}
193+
194+
/**
195+
* Create an IT Contact
196+
*
197+
* Add an IT Contact to an Organization. No Admin Portal invitation is sent,
198+
* though the contact is notified if the Organization has a connection
199+
* certificate nearing expiry.
200+
* @param options - Object containing the Organization ID and the email address.
201+
* @returns {Promise<ItContact>}
202+
* @throws {AuthorizationException} 403
203+
* @throws {NotFoundException} 404
204+
* @throws {ConflictException} 409
205+
* @throws {UnprocessableEntityException} 422
206+
*/
207+
async createItContact(options: CreateItContactOptions): Promise<ItContact> {
208+
const { organizationId, ...payload } = options;
209+
210+
const { data } = await this.workos.post<ItContactResponse>(
211+
`/organizations/${organizationId}/it_contacts`,
212+
serializeCreateItContactOptions(payload),
213+
);
214+
215+
return deserializeItContact(data);
216+
}
217+
218+
/**
219+
* Delete an IT Contact
220+
*
221+
* Remove an IT Contact from an Organization and revoke the contact's active
222+
* setup links.
223+
* @param options - Object containing the Organization ID and the IT Contact ID.
224+
* @returns {Promise<void>}
225+
* @throws {AuthorizationException} 403
226+
* @throws {NotFoundException} 404
227+
*/
228+
async deleteItContact(options: DeleteItContactOptions): Promise<void> {
229+
const { organizationId, contactId } = options;
230+
231+
await this.workos.delete(
232+
`/organizations/${organizationId}/it_contacts/${contactId}`,
233+
);
234+
}
235+
236+
/**
237+
* Invite an IT Contact
238+
*
239+
* Create an Admin Portal setup link and email it to the IT Contact. An
240+
* Organization can have at most one active invitation.
241+
* @param options - Object containing the Organization ID, the IT Contact ID and the intents.
242+
* @returns {Promise<void>}
243+
* @throws {AuthorizationException} 403
244+
* @throws {NotFoundException} 404
245+
* @throws {ConflictException} 409
246+
* @throws {UnprocessableEntityException} 422
247+
*/
248+
async inviteItContact(options: InviteItContactOptions): Promise<void> {
249+
const { organizationId, contactId, ...payload } = options;
250+
251+
await this.workos.post(
252+
`/organizations/${organizationId}/it_contacts/${contactId}/invite`,
253+
serializeInviteItContactOptions(payload),
254+
);
255+
}
256+
257+
/**
258+
* Revoke an IT Contact's invitation
259+
*
260+
* Revoke the Organization's active Admin Portal invitation.
261+
* @param options - Object containing the Organization ID and the IT Contact ID.
262+
* @returns {Promise<void>}
263+
* @throws {AuthorizationException} 403
264+
* @throws {NotFoundException} 404
265+
*/
266+
async revokeItContact(options: RevokeItContactOptions): Promise<void> {
267+
const { organizationId, contactId } = options;
268+
269+
await this.workos.post(
270+
`/organizations/${organizationId}/it_contacts/${contactId}/revoke`,
271+
{},
272+
);
273+
}
154274
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './create-organization-options.serializer';
2+
export * from './it-contact-options.serializer';
3+
export * from './it-contact.serializer';
24
export * from './organization.serializer';
35
export * from './update-organization-options.serializer';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {
2+
CreateItContactOptions,
3+
InviteItContactOptions,
4+
SerializedCreateItContactOptions,
5+
SerializedInviteItContactOptions,
6+
} from '../interfaces';
7+
8+
export const serializeCreateItContactOptions = (
9+
options: Omit<CreateItContactOptions, 'organizationId'>,
10+
): SerializedCreateItContactOptions => ({
11+
email: options.email,
12+
});
13+
14+
export const serializeInviteItContactOptions = (
15+
options: Omit<InviteItContactOptions, 'organizationId' | 'contactId'>,
16+
): SerializedInviteItContactOptions => ({
17+
intents: options.intents,
18+
});

0 commit comments

Comments
 (0)