UNPKG

lever-js

Version:

Unofficial JS SDK for Lever API

1,266 lines (1,145 loc) 26.6 kB
// --- Core types --- type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; /** * A generic description of one API endpoint. * * @template P Path parameters object * @template Q Query‐string parameters object * @template B Request JSON body parameters object * @template R Response payload type */ interface Endpoint<P, Q, B, R> { method: HttpMethod; path: string; /** * Invoke this endpoint. * * @param apiKey Your Lever API key (Basic auth username; password is empty) * @param params Path parameters * @param query Optional query parameters * @param body Optional JSON body * @param init Optional fetch init overrides */ call: ( apiKey: string, data: (P extends null ? {} : { params: P; }) & (B extends null ? {} : { body?: B }) & (Q extends null ? {} : { query?: Q }), init?: Omit<RequestInit, 'body' | 'method'> ) => Promise<R>; } // Base URL const ROOT = 'https://api.lever.co/v1'; /** * Factory to build a strongly‐typed Endpoint with Basic auth passed to `call`. */ function createEndpoint<P extends null | Record<string, string>, Q, B, R>( config: Omit<Endpoint<P, Q, B, R>, 'call'> ): Endpoint<P, Q, B, R>['call'] { const call: Endpoint<P, Q, B, R>['call'] = async ( apiKey, data, init = {} ) => { // 1. interpolate path params let url = config.path; for (const [key, val] of Object.entries( 'params' in data ? data.params : {} )) { url = url.replace(`:${key}`, encodeURIComponent(val)); } // 2. build query string const qs = new URLSearchParams( Object.entries('query' in data ? (data.query ?? []) : []) .filter(([, v]) => v != null) .map(([k, v]) => [k, String(v)]) ).toString(); const fullUrl = `${ROOT}${url}${qs ? '?' + qs : ''}`; // 3. prepare headers (Basic auth + JSON if needed) const defaultHeaders: Record<string, string> = { Authorization: `Basic ${btoa(apiKey + ':')}`, }; if (config.method !== 'GET') { defaultHeaders['Content-Type'] = 'application/json'; } // allow user to override/augment headers via init.headers const mergedHeaders = { ...defaultHeaders, ...((init as any).headers ?? {}), }; // 4. perform fetch const res = await fetch(fullUrl, { method: config.method, headers: mergedHeaders, ...(config.method === 'GET' || !('body' in data) ? {} : { body: JSON.stringify(data.body) }), ...init, }); if (!res.ok) { throw new Error( `${config.method} ${fullUrl} HTTP ${res.status}: ${res.statusText}` ); } return (await res.json()) as R; }; return call; } export interface Application { id: string; opportunityId: string; candidateId: string; createdAt: number; type: string; posting: string; user?: string; name?: string; email?: string; phone?: { type: any; value: string; }; company: any; links: any; comments?: string; resume: any; customQuestions?: Array<{ accountId?: string; createdAt: number; text: string; description: string; type: string; fields: Array<{ type: string; required: boolean; text: string; description: string; options?: Array<{ text: string; }>; isSummary?: boolean; summaryText?: string; value: any; prompt?: string; }>; baseTemplateId: string; referrerId?: string; userId?: string; user?: string; stage: any; completedAt?: number; }>; requisitionForHire?: { id: string; requisitionCode: string; hiringManagerOnHire: string; }; ownerId?: string; hiringManager?: string; } export interface ArchiveReason { id: string; text: string; status: string; type: string; } export interface AuditEvent { id: string; createdAt: number; type: string; user: { id: string; email: string; name: string; role: string; }; target: { id: string; type: string; label: string; }; meta: { authentication?: { method: string; error: { message: string; type: string; }; }; user: { id: string; email: string; name: string; role: string; }; }; } // export interface Candidate { // [key: string]: any; // } export interface Contact { id: string; name: string; headline: string; isAnonymized: boolean; location: { name: string; }; emails: Array<string>; phones: Array<{ type: string; value: string; }>; } // export interface EeoResponse { // [key: string]: any; // } export interface Feedback { id: string; type: string; text: string; instructions: string; fields: Array<{ id: string; type: string; text: string; description: string; required: boolean; value: any; prompt?: string; options?: Array<{ text: string; }>; }>; baseTemplateId: string; interview: string; panel: string; user: string; createdAt: number; updatedAt: number; completedAt: number; deletedAt: number; } export interface FeedbackTemplate { id: string; text: string; group?: { id: string; name: string; }; createdAt: number; updatedAt: number; instructions: string; stage?: { id: string; text: string; }; fields: Array<{ id: string; description: string; options: Array<{ text: string; }>; prompt: string; required: boolean; text: string; type: string; }>; } export interface FileObject { id: string; downloadUrl: string; ext: string; name: string; uploadedAt: string; status: string; size: string; } export interface Interview { id: string; panel: string; subject: string; note: string; interviewers: Array<{ email: string; id: string; name: string; feedbackTemplate: string; }>; timezone: string; createdAt: number; date: number; duration: number; location: string; feedbackTemplate: string; feedbackForms: Array<string>; feedbackReminder: string; user: string; stage: string; canceledAt: any; postings: Array<string>; } export interface Note { id: string; text: string; fields: Array<{ type: string; text: string; value: string; createdAt: number; user: string; score: number; stage: string; }>; user: string; secret: boolean; completedAt: number; createdAt: number; deletedAt: number; } export interface Offer { id: string; createdAt: number; status: string; creator: string; fields: Array<{ text: string; identifier: string; value: any; }>; sentDocument: { fileName: string; uploadedAt: number; downloadUrl: string; }; signedDocument?: { fileName: string; uploadedAt: number; downloadUrl: string; }; } export interface OpportunityExpandApplication { id: string; type: string; candidateId: string; opportunityId: string; posting: string; postingHiringManager: any; postingOwner: string; name: string; company: any; phone: { type: any; value: string; }; email: string; links: Array<any>; comments: string; user: any; customQuestions: Array<any>; createdAt: number; archived: any; requisitionForHire: any; } export interface Opportunity { id: string; name: string; headline: string; contact: string; emails: Array<string>; phones: Array<{ value: string; }>; confidentiality?: string; location: string; links: Array<string>; createdAt: number; updatedAt?: number; lastInteractionAt: number; lastAdvancedAt: number; snoozedUntil: number; archivedAt: any; archiveReason: any; stage: string; stageChanges?: Array<{ toStageId: string; toStageIndex: number; userId: string; updatedAt: number; }>; owner: string; tags: Array<string>; sources: Array<string>; origin: string; sourcedBy?: string; applications: Array<string> | Array<OpportunityExpandApplication>; resume: any; followers: Array<string>; urls?: { list: string; show: string; }; dataProtection?: { store: { allowed: boolean; expiresAt: number; }; contact: { allowed: boolean; expiresAt: any; }; }; isAnonymized?: boolean; } export interface Panel { id: string; applications: Array<string>; canceledAt: any; createdAt: number; end: number; externallyManaged: boolean; externalUrl: string; interviews: Array<{ id: string; date: number; duration: number; feedbackReminder: string; feedbackTemplate: string; interviewers: Array<{ email: string; id: string; name: string; feedbackTemplate: string; }>; location: string; note: string; subject: string; }>; note: string; stage: string; start: number; timezone: string; user: string; } export interface Posting { id: string; text: string; createdAt: number; updatedAt: number; user: string; owner: string; hiringManager: string; confidentiality?: string; categories: { team: string; department?: string; location: string; allLocations: Array<string>; commitment: string; level?: string; }; content: { description: string; descriptionHtml: string; lists: Array<{ text: string; content: string; }>; closing: string; closingHtml: string; }; country?: string; tags: Array<any>; state: string; distributionChannels: Array<string>; reqCode?: string; requisitionCodes: Array<string>; salaryDescription: string; salaryDescriptionHtml: string; salaryRange: { max: number; min: number; currency: string; interval: string; }; urls: { list: string; show: string; apply: string; }; workplaceType: string; } export interface User { /** Unique ID for the user */ id: string; /** User's preferred name */ name: string; /** Username, typically from email prefix */ username: string; /** User's email address */ email: string; /** Epoch timestamp when user was created */ createdAt: number; /** Epoch timestamp when user was deactivated, or null if active */ deactivatedAt: number | null; /** User's access role */ accessRole: | 'super admin' | 'admin' | 'team member' | 'limited team member' | 'interviewer'; /** URL to user's gravatar image, if enabled */ photo: string; /** Optional: Unique ID from external HR directory */ externalDirectoryId?: string; /** Optional: Array of linked contact IDs */ linkedContactIds?: string[]; /** Optional: User's job title */ jobTitle?: string; /** Optional: User's manager ID */ managerId?: string; } export interface Stage { id: string; text: string; } export type ListResponse<T> = | { data: T[]; hasNext: false; } | { data: T[]; hasNext: true; next: string; }; export type ListQuery = { limit?: number | string; offset?: string; expand?: string; }; export type Response<T> = { data: T }; export type RetrieveApplicationParams = { opportunity: string; application: string; }; export type RetrieveApplicationResponse = Response<Application>; /** * @deprecated Use `retrieveOpportunity` with `expand` set to `applications`. */ export const retrieveApplication = createEndpoint< RetrieveApplicationParams, null, null, RetrieveApplicationResponse >({ method: 'GET', path: '/opportunities/:opportunity/applications/:application', }); export type ListApplicationsParams = { opportunity: string; }; export type ListApplicationsResponse = ListResponse<Application>; export const listApplications = createEndpoint< ListApplicationsParams, ListQuery, null, ListApplicationsResponse >({ method: 'GET', path: '/opportunities/:opportunity/applications', }); export type ApplyToPostingParams = { posting: string; }; export type ApplyToPostingQuery = { send_confirmation_email?: 'true' | 'false'; }; export type ApplyToPostingBody = { customQuestions: Array<{ id: string; fields: Array<{ value: any; }>; }>; eeoResponses: { gender: string; race: string; veteran: string; disability: string; disabilitySignature: string; disabilitySignatureDate: string; }; diversitySurvey?: { surveyId: string; candidateSelectedLocation: string; responses: Array<{ questionId: string; questionText: string; questionType: string; answer: string; }>; }; ipAddress?: string; source?: string; consent?: { marketing: boolean; }; origin?: string; personalInformation?: Array<{ name: string; value: string; }>; urls: Array<{ name: string; value: string; }>; }; export type ApplyToPostingResponse = Response<Application>; export const applyToPosting = createEndpoint< ApplyToPostingParams, ApplyToPostingQuery, ApplyToPostingBody, ApplyToPostingResponse >({ method: 'POST', path: '/postings/:posting/apply', }); export const createApplication = applyToPosting; export type RetrievePostingParams = { posting: string; }; export type RetrievePostingQuery = { distribution?: 'internal' | 'external'; }; export type RetrievePostingResponse = Response<Posting>; export const retrievePosting = createEndpoint< RetrievePostingParams, RetrievePostingQuery, null, RetrievePostingResponse >({ method: 'GET', path: '/postings/:posting', }); export type RetrieveInterviewParams = { opportunity: string; interview: string; }; export type RetrieveInterviewResponse = Response<Interview>; export const retrieveInterview = createEndpoint< RetrieveInterviewParams, null, null, RetrieveInterviewResponse >({ method: 'GET', path: '/opportunities/:opportunity/interviews/:interview', }); export type ListInterviewsParams = { opportunity: string; }; export type ListInterviewsResponse = ListResponse<Interview>; export const listInterviews = createEndpoint< ListInterviewsParams, ListQuery, null, ListInterviewsResponse >({ method: 'GET', path: '/opportunities/:opportunity/interviews', }); export type CreateInterviewParams = { opportunity: string; }; export type CreateInterviewQuery = { /** * Perform this create on behalf of a specified user. If unspecified, defaults to null. */ perform_as?: string; }; export type CreateInterviewBody = { panel: string; subject?: string; note?: string; interviewers: Array<{ id: string; feedbackTemplate: string; }>; date: number; duration: number; location?: string; feedbackTemplate?: string; feedbackReminder?: string; }; export type CreateInterviewResponse = Response<Interview>; export const createInterview = createEndpoint< CreateInterviewParams, CreateInterviewQuery, CreateInterviewBody, CreateInterviewResponse >({ method: 'POST', path: '/opportunities/:opportunity/interviews', }); export type UpdateInterviewParams = { opportunity: string; interview: string; }; export type UpdateInterviewQuery = CreateInterviewQuery; export type UpdateInterviewBody = Partial<CreateInterviewBody>; export type UpdateInterviewResponse = CreateInterviewResponse; export const updateInterview = createEndpoint< UpdateInterviewParams, null, UpdateInterviewBody, UpdateInterviewResponse >({ method: 'PUT', path: '/opportunities/:opportunity/interviews/:interview', }); export type DeleteInterviewParams = { opportunity: string; interview: string; }; export type DeleteInterviewQuery = CreateInterviewQuery; export const deleteInterview = createEndpoint< DeleteInterviewParams, DeleteInterviewQuery, null, null >({ method: 'DELETE', path: '/opportunities/:opportunity/interviews/:interview', }); export type RetrieveNoteParams = { opportunity: string; note: string; }; export type RetrieveNoteResponse = Response<Note>; export const retrieveNote = createEndpoint< RetrieveNoteParams, null, null, RetrieveNoteResponse >({ method: 'GET', path: '/opportunities/:opportunity/notes/:note', }); export type ListNotesParams = { opportunity: string; }; export type ListNotesResponse = ListResponse<Note>; export const listNotes = createEndpoint< ListNotesParams, ListQuery, null, ListNotesResponse >({ method: 'GET', path: '/opportunities/:opportunity/notes', }); export type CreateNoteParams = { opportunity: string; }; export type CreateNoteQuery = { /** * Perform this create on behalf of a specified user. If unspecified, defaults to null. */ perform_as?: string; /** * Add a comment to an existing note of a specific opportunity. Comments must have a value, and can optionally take a createdAt timestamp */ note_id?: string; }; export type CreateNoteBody = { value: string; /** * If true, note will only be visible to users with Sensitive Information Privileges (SIP) for postings applied to candidate and users who have been @-mentioned. If unspecified, defaults to false. */ secret?: boolean; /** * Score value is an integer between 1 and 4. * 1 - Strong No (double thumbs down) * 2 - No (single thumb down) * 3 - Yes (single thumb up) * 4 - Strong Yes (double thumbs up). * If unspecified, defaults to null. */ score?: number; /** * If true, creation of this note will send notifications to all users following the candidate. If unspecified, defaults to false. */ notifyFollowers?: boolean; /** * Timestamp in milliseconds */ createdAt?: number; }; export type CreateNoteResponse = Response<Note>; export const createNote = createEndpoint< CreateNoteParams, CreateNoteQuery, CreateNoteBody, CreateNoteResponse >({ method: 'POST', path: '/opportunities/:opportunity/notes', }); export type UpdateNoteParams = { opportunity: string; note: string; }; export type UpdateNoteBody = { value: string[]; user: string; secret?: boolean; createdAt?: number; completedAt?: number; score?: number; }; export type UpdateNoteResponse = CreateNoteResponse; export const updateNote = createEndpoint< UpdateNoteParams, null, UpdateNoteBody, UpdateNoteResponse >({ method: 'PUT', path: '/opportunities/:opportunity/notes/:note', }); export type DeleteNoteParams = { opportunity: string; note: string; }; export const deleteNote = createEndpoint<DeleteNoteParams, null, null, null>({ method: 'DELETE', path: '/opportunities/:opportunity/notes/:note', }); export type RetrieveOpportunityParams = { opportunity: string; }; export type RetrieveOpportunityResponse = Response<Opportunity>; export const retrieveOpportunity = createEndpoint< RetrieveOpportunityParams, { expand?: string; }, null, RetrieveOpportunityResponse >({ method: 'GET', path: '/opportunities/:opportunity', }); export type ListOpportunitiesQuery = ListQuery & { include?: string; expand?: | 'applications' | 'stage' | 'owner' | 'followers' | 'sourcedBy' | 'contact'; tag?: string; email?: string; origin?: string; source?: string; confidentiality?: string; stage_id?: string; posting_id?: string; archived_posting_id?: string; created_at_start?: number; created_at_end?: number; updated_at_start?: number; updated_at_end?: number; advanced_at_start?: number; advanced_at_end?: number; archived?: boolean; archive_reason_id?: string; snoozed?: boolean; contact_id?: string; }; export type ListOpportunitiesResponse = ListResponse<Opportunity>; export const listOpportunities = createEndpoint< null, ListOpportunitiesQuery, null, ListOpportunitiesResponse >({ method: 'GET', path: '/opportunities', }); export type ListDeletedOpportunitiesQuery = { deleted_at_start?: number; deleted_at_end?: number; }; export const listDeletedOpportunities = createEndpoint< null, ListDeletedOpportunitiesQuery, null, ListOpportunitiesResponse >({ method: 'GET', path: '/opportunities/deleted', }); export type CreateOpportunityQuery = { perform_as: string; parse?: boolean; perform_as_posting_owner?: boolean; }; export type CreateOpportunityBody = { /** Contact full name */ name: string; /** Contact headline (e.g. companies or schools, or parsed from a resume) */ headline: string; /** * Stage UID of this Opportunity’s current stage. * If omitted, defaults to the “New Lead” stage. */ stage?: string; /** Contact current location */ location: string; /** Optional contact phone number(s) */ phones?: { /** The phone number (e.g. "(123) 456-7891") */ value: string; /** One of: "mobile", "home", "work", "skype", "other" */ type?: 'mobile' | 'home' | 'work' | 'skype' | 'other'; }[]; /** Contact email address(es) */ emails: string[]; /** Contact links (e.g. personal website, LinkedIn) */ links: string[]; /** Tags to apply to this Opportunity */ tags: string[]; /** Sources to apply to this Opportunity */ sources: string[]; /** How this Opportunity was added to Lever */ origin: | 'agency' | 'applied' | 'internal' | 'referred' | 'sourced' | 'university'; /** * UID of the user who owns this Opportunity. * Defaults to the perform_as user if omitted. */ owner?: string; /** * UIDs of users to add as followers on this Opportunity. * The creator is always added automatically. */ followers?: string[]; /** Resume file (binary). Only supported in multipart/form-data requests. */ resumeFile?: File; /** Additional file(s) for this Opportunity. multipart/form-data only. */ files?: File[]; /** * Array of Posting UIDs. * ⚠️ Only one per request—send multiple requests to associate multiple postings. */ postings: [string]; /** * When the Opportunity was created (ms since epoch). * Use to import historical data; defaults to now if omitted. */ createdAt?: number; /** * Archive metadata. If provided, archives the Opportunity on creation. * - `archivedAt` (ms since epoch): when it was archived (defaults to now if omitted) * - `reason`: UID of the archive reason (required) */ archived?: { archivedAt?: number; reason: string; }; /** * UID of an existing contact to link to this Opportunity. * If omitted, Lever will attempt to dedupe by the provided email(s). */ contact?: string; }; export type CreateOpportunityResponse = Response<Opportunity>; export const createOpportunity = createEndpoint< null, CreateOpportunityQuery, CreateOpportunityBody, CreateOpportunityResponse >({ method: 'POST', path: '/opportunities', }); export type UpdateOpportunityStageParams = { opportunity: string; }; export type UpdateOpportunityStageBody = { stage: string; }; export const updateOpportunityStage = createEndpoint< UpdateOpportunityStageParams, null, UpdateOpportunityStageBody, null >({ method: 'PUT', path: '/opportunities/:opportunity/stage', }); export type UpdateOpportunityArchivedStateParams = { opportunity: string; }; export type UpdateOpportunityArchivedStateBody = { reason: string; cleanInterviews?: boolean; requisitionId?: string; }; /** ***Update opportunity archived state** * Update an Opportunity's archived state. If an Opportunity is already archived, its archive reason can be changed or if null is specified as the reason, it will be unarchived. If an Opportunity is active, it will be archived with the reason provided. * * The requisitionId is optional. If the provided reason maps to ‘Hired’ and a requisition is provided, the Opportunity will be marked as Hired, the active offer is removed from the requisition, and the hired count for the requisition will be incremented. * * If a requisition is specified and there are multiple active applications on the profile, you will receive an error. If the specific requisition is closed, you will receive an error. If there is an offer extended, it must be signed, and the offer must be associated with an application for a posting linked to the provided requisition. You can hire a candidate against a requisition without an offer. */ export const updateOpportunityArchivedState = createEndpoint< UpdateOpportunityArchivedStateParams, null, UpdateOpportunityArchivedStateBody, null >({ method: 'PUT', path: '/opportunities/:opportunity/archived', }); export type AddOpportunityContactLinksParams = { opportunity: string; }; export type AddOpportunityContactLinksBody = { links: string[]; }; export const addContactLinksByOpportunity = createEndpoint< AddOpportunityContactLinksParams, null, AddOpportunityContactLinksBody, null >({ method: 'POST', path: '/opportunities/:opportunity/addLinks', }); export type RemoveOpportunityContactLinksParams = AddOpportunityContactLinksParams; export type RemoveOpportunityContactLinksBody = AddOpportunityContactLinksBody; export const removeContactLinksByOpportunity = createEndpoint< RemoveOpportunityContactLinksParams, null, RemoveOpportunityContactLinksBody, null >({ method: 'POST', path: '/opportunities/:opportunity/removeLinks', }); export type AddOpportunityTagsParams = { opportunity: string; }; export type AddOpportunityTagsBody = { tags: string[]; }; export const addOpportunityTags = createEndpoint< AddOpportunityTagsParams, null, AddOpportunityTagsBody, null >({ method: 'POST', path: '/opportunities/:opportunity/addTags', }); export type RemoveOpportunityTagsParams = AddOpportunityTagsParams; export type RemoveOpportunityTagsBody = AddOpportunityTagsBody; export const removeOpportunityTags = createEndpoint< RemoveOpportunityTagsParams, null, RemoveOpportunityTagsBody, null >({ method: 'POST', path: '/opportunities/:opportunity/removeTags', }); export type AddOpportunitySourcesParams = { opportunity: string; }; export type AddOpportunitySourcesBody = { sources: string[]; }; export const addOpportunitySources = createEndpoint< AddOpportunitySourcesParams, null, AddOpportunitySourcesBody, null >({ method: 'POST', path: '/opportunities/:opportunity/addSources', }); export type RemoveOpportunitySourcesParams = AddOpportunitySourcesParams; export type RemoveOpportunitySourcesBody = AddOpportunitySourcesBody; export const removeOpportunitySources = createEndpoint< RemoveOpportunitySourcesParams, null, RemoveOpportunitySourcesBody, null >({ method: 'POST', path: '/opportunities/:opportunity/removeSources', }); export type GetStageParams = { stage: string; }; export type GetStageResponse = Response<Stage>; export const getStage = createEndpoint< GetStageParams, null, null, GetStageResponse >({ method: 'GET', path: '/stages/:stage', }); export type GetStagesResponse = ListResponse<Stage>; export const getStages = createEndpoint<null, null, null, GetStagesResponse>({ method: 'GET', path: '/stages', }); export const getTags = createEndpoint<null, null, null, null>({ method: 'GET', path: '/tags', }); export type GetArchiveReasonParams = { archiveReason: string; }; export type GetArchiveReasonResponse = Response<ArchiveReason>; export const getArchiveReason = createEndpoint< GetArchiveReasonParams, null, null, GetArchiveReasonResponse >({ method: 'GET', path: '/archive_reasons', }); export type GetArchiveReasonsResponse = ListResponse<ArchiveReason>; export type GetArchiveReasonsQuery = { type?: 'hired' | 'non-hired'; }; export const getArchiveReasons = createEndpoint< null, GetArchiveReasonsQuery, null, GetArchiveReasonsResponse >({ method: 'GET', path: '/archive_reasons', });