# Authentication (/careers/v1/getting-started/authentication) The Careers API authenticates with a **Career API key** passed in the `X-API-KEY` header. Career keys are limited to non-sensitive vacancy data, so they're safe to embed in a public-facing website. ```bash curl https://app.peopleforce.io/api/careers/v1/vacancies \ -H "X-API-KEY: " ``` Scaffold page — fuller content coming soon. # Introduction (/careers/v1/getting-started) The **Careers API** exposes public-facing recruitment data — open vacancies, locations, and employment types — so you can power a custom careers site or syndicate openings to a job board. It is limited to non-sensitive, vacancy-specific information and authenticates with a **Career API key**, so it's safe to use from a public-facing website. Scaffold page — fuller content coming soon. Use the version switcher at the top of the sidebar to move between the Company and Careers APIs, and browse the **API reference** in the sidebar for the available endpoints. You'll need a Career API key first — see [Authentication](./authentication.mdx). # Request a feature (/careers/v1/getting-started/request-a-feature) Didn't find what you were looking for in the API or webhooks? Let us know. We keep the API broad and already cover the common use cases — but if something is missing, send us a feature request and we'll see whether it can be added. [Open the requests board →](https://feedback.peopleforce.io/b/for-devs) # Pagination (/careers/v1/api-basics/pagination) List endpoints (such as vacancies) are paginated. Each response wraps its results in a `data` array alongside a `metadata` object describing the current page and the total result set. The default page size is **25**. ```json { "data": [], "metadata": { "page": "1", "per_page": "25", "total_pages": "4", "total_count": "92" } } ``` ## Metadata fields [#metadata-fields] | Field | Description | | :------------ | :-------------------------------------- | | `page` | The current page number. | | `per_page` | Number of items returned per page. | | `total_pages` | Total number of pages available. | | `total_count` | Total number of items across all pages. | ## Query parameters [#query-parameters] | Parameter | Description | | :--------- | :----------------------------------------------- | | `page` | The page number to fetch. Defaults to `1`. | | `per_page` | Number of results per page. Defaults to `25`. | | `offset` | Skip a number of results before the page starts. | ```bash curl "https://app.peopleforce.io/api/careers/v1/vacancies?page=2&per_page=50" \ -H "X-API-KEY: " ``` To walk the full result set, request `page=1` and keep incrementing `page` until it reaches `total_pages`. # Job boards integration (/careers/v1/guides/job-boards) This guide is for job board platforms (e.g. Indeed-style sites where many companies post openings) that want to integrate with PeopleForce using an API key. It covers the endpoints, workflow, and best practices for syncing job postings and candidate applications. Building a careers page for a single company's own website instead? See [Own career site integration](./own-career-site.mdx). **Endpoint availability.** This workflow relies on PeopleForce recruitment endpoints (vacancies and candidates). These are **not yet part of the public Company API v4 surface** documented here. Confirm the available endpoints and base path with the PeopleForce team before building. The workflow below is preserved from the previous integration guide. ## Integration workflow [#integration-workflow] The customer's essential tasks are: * Publish a job from PeopleForce to your platform. * Receive candidate applications and sync them back to PeopleForce. Suggested setup flow: 1. Add a settings field where customers paste their PeopleForce API key. 2. When a customer creates a job posting, let them pick from their open PeopleForce vacancies and pre-fill the details. 3. After publishing, route applicant data back to the correct PeopleForce vacancy and mark the source. ## Getting started: API key [#getting-started-api-key] 1. In PeopleForce, go to **Settings → Security → API keys**. 2. Generate a new key and store it securely. 3. See [Authentication](../getting-started/authentication.mdx) for details. One API key can be used across multiple accounts of the same customer (e.g. different departments or business units). ## Importing job data [#importing-job-data] 1. Prompt the customer to connect their account with an API key. 2. When creating a vacancy on your side, suggest selecting from existing PeopleForce vacancies. 3. Retrieve open vacancies and display those in a published state (`state=opened`). 4. Fetch the selected vacancy's details and import them (title, description, recruiter, salary, etc.). ### Vacancy data mapping [#vacancy-data-mapping] Either auto-fill your job form from the imported vacancy (requires pre-mapping fields between systems) or let customers map fields manually. ## Submitting candidates [#submitting-candidates] When a candidate applies: 1. Collect their information (name, email, phone, CV file, cover letter, etc.). 2. Send it to PeopleForce, linked to the correct vacancy. 3. Include a `source` value (e.g. your platform name) for reporting. 4. Use the applications parameter to assign the candidate to the right vacancy stage. 5. If the same candidate applies to another vacancy at the same company, update the existing candidate (by ID) and add another entry to its applications. ### Candidate de-duplication [#candidate-de-duplication] * PeopleForce auto-checks for duplicates by email and CV file. * If a duplicate is found, the candidate's information is updated. * Manual updates via the UI are also possible. ## Test environment [#test-environment] 1. Request access to the PeopleForce test environment. 2. Use test data to simulate job sync and candidate submissions. 3. When complete, schedule a live demo with the PeopleForce team and share sandbox access for QA. ## Best practices [#best-practices] * Validate the API key before initiating syncs. * Clearly mark PeopleForce-originated jobs in your interface. * Require applications via your forms (not email or redirects) to capture complete data. * Log API errors and retry failed requests automatically. For test access, technical support, or feedback, contact us via [peopleforce.io/partners](https://peopleforce.io/partners). # Own career site integration (/careers/v1/guides/own-career-site) This guide shows how to display your open PeopleForce vacancies on your own website. Whether you already have a careers page or are building one, you don't have to use the careers site provided by PeopleForce. The last section covers taking the application on your own site too. If you represent a job board and want to integrate your product with PeopleRecruit, see [Job boards integration](./job-boards.mdx) instead. ## Prerequisites [#prerequisites] Use the **Careers API** (`https://app.peopleforce.io/api/careers/v1`), which exposes only non-sensitive vacancy data. You'll need a **Career API key** — see [Authentication](../getting-started/authentication.mdx). Only ever use a **Career** API key on a public-facing website. A Company API key would expose worker data. ## Recommended flow [#recommended-flow] 1. Retrieve the list of vacancies from PeopleForce. 2. Retrieve filter data (locations and employment types) so candidates can filter. 3. Link each vacancy to the PeopleForce careers site to apply. ### 1. Retrieve the list of vacancies [#1-retrieve-the-list-of-vacancies] PeopleForce is the source of truth for your open vacancies. ```bash curl https://app.peopleforce.io/api/careers/v1/vacancies \ -H "X-API-KEY: " ``` This returns the list of open vacancies. ### 2. Retrieve locations and employment types [#2-retrieve-locations-and-employment-types] Use these to build drop-down filters; both can be used to filter the vacancies list: * `GET /api/careers/v1/locations` * `GET /api/careers/v1/employment_types` ### 3. Link out to apply [#3-link-out-to-apply] Building a robust application form (file upload, field validation, and so on) is complex, so we strongly recommend directing candidates to apply on the PeopleForce careers site. Each vacancy in the **List vacancies** and **Get a vacancy** responses includes an `apply_url` you can link to directly. If the candidate has to stay on your own domain, see [Accept applications on your own site](#accept-applications-on-your-own-site-server-side-integration) below. ## Accept applications on your own site (server-side integration) [#accept-applications-on-your-own-site-server-side-integration] To keep the form on your own domain, submit applications through the PeopleForce API instead of linking out. **This is a server-side integration.** It needs a **Company API key**, which grants full access to your account. The key must never reach the browser — not in JavaScript, not in a build artifact, not in a frontend environment variable. Your page posts to your own backend, and your backend calls PeopleForce. ### What you need [#what-you-need] * A **Company API key** — Settings → API keys → Generate API key. See [Authentication](/company/v3/getting-started/authentication). * A backend that can keep the key secret and hold an uploaded file long enough to forward it. * The vacancy `id`, which you already have from the Careers API vacancies list. The ids are the same across both APIs. ### How it works [#how-it-works] Submitting an application takes two calls: 1. **Create the candidate** — [`POST /api/public/v3/recruitment/candidates`](/company/v3/reference/candidates/create-recruitment-candidate) 2. **Attach the candidate to the vacancy** — [`POST /api/public/v3/recruitment/vacancies/{vacancy_id}/applications`](/company/v3/reference/vacancies/create-recruitment-vacancy-application) Treat the API reference as authoritative for the full field list and response schema. This guide covers what the reference does not: the order of the calls, the flags that matter, and the failure modes. Before building the form, fetch the custom fields your recruiters expect — see [Custom fields](#custom-fields). ### Step 1 — Create the candidate [#step-1--create-the-candidate] Reference: [Create a candidate](/company/v3/reference/candidates/create-recruitment-candidate) ```http POST https://app.peopleforce.io/api/public/v3/recruitment/candidates X-API-KEY: Content-Type: application/json ``` ```json { "full_name": "Ada Lovelace", "email": "ada@example.com", "phone_numbers": ["+44 12 3456 6789"], "cover_letter": "I am applying for the Senior Engineer role because…", "location": "London, UK", "urls": ["https://github.com/ada"], "source_id": 12, "consented_at": "2026-08-16T10:32:00Z", "resume": "data:application/pdf;name=ada-cv.pdf;base64,JVBERi0xLjQKJc..." } ``` Only `full_name` is required. A candidate with neither `email` nor `phone_numbers` is unusable to a recruiter, so require at least one contact method on your own form. | Field | Notes | | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `full_name` | **Required.** Max 50 characters. Cannot contain `` ` `` `!` `@` `#` `$` `%` `^` `&` `*` `+` `_` `=`. | | `email` | Must be valid, and must not already belong to another candidate — see [Handling duplicates](#handling-duplicates). | | `phone_numbers` | Array of strings. Digits, spaces, `(`, `)`, `+`, `.`, `-` only — no letters, no `x123` extensions. | | `cover_letter` | Plain text or HTML. | | `resume` | Base64 data URI — see [Uploading a CV](#uploading-a-cv). | | `consented_at`, `future_recruitment_consented_at` | ISO 8601 timestamps — see [Recording consent](#recording-consent). | | `source_id` | Where the application came from — see [Tracking the source](#tracking-the-source). | | `location`, `position`, `date_of_birth`, `skills`, `urls`, `telegram_username`, `desired_salary` + `currency_code`, `seniority_id`, `gender_id`, `tag_ids` | Optional. Some are hidden unless the key may see sensitive candidate fields. | Returns `201 Created`. Keep `data.id` for step 2. ```json { "data": { "id": 84213, "full_name": "Ada Lovelace", "email": "ada@example.com", "resume": true, "resume_url": "https://…", "applications": [], "custom_fields": [] } } ``` ### Step 2 — Attach the candidate to the vacancy [#step-2--attach-the-candidate-to-the-vacancy] Reference: [Create a vacancy application](/company/v3/reference/vacancies/create-recruitment-vacancy-application) ```http POST https://app.peopleforce.io/api/public/v3/recruitment/vacancies/{vacancy_id}/applications X-API-KEY: Content-Type: application/json ``` ```json { "applicant_id": 84213, "perform_automations": true } ``` **Pass `perform_automations: true`.** It defaults to off, and it runs the vacancy's pipeline automations — including the acknowledgement email to the candidate, if any are configured. Without it the application lands in the first pipeline stage and the candidate hears nothing. `applicant_state_id` starts the application in a specific pipeline stage instead of the first. Get the ids from [List pipelines](/company/v3/reference/vacancies/list-recruitment-pipelines). The vacancy must be **published**. A draft, closed, or archived vacancy returns `404`. ```json { "data": { "id": 55123, "applicant": { "id": 84213, "full_name": "Ada Lovelace", "email": "ada@example.com", "phone_numbers": ["+44 12 3456 6789"] }, "pipeline_state": { "id": 901, "name": "Applied" }, "created_at": "2026-08-16T10:32:04.000Z", "updated_at": "2026-08-16T10:32:04.000Z" } } ``` Applications created through the API do **not** notify the vacancy's collaborators and hiring lead, unlike the PeopleForce-hosted form. The candidate appears in the pipeline as normal and the `vacancy_application_create` [webhook](/company/v3/webhooks/candidates-and-vacancies) still fires — use that webhook if your team needs an immediate alert. ### Uploading a CV [#uploading-a-cv] `resume` takes the file inline as a base64 data URI, in exactly this shape: ```http data:;name=;base64, ``` Example: `data:application/pdf;name=ada-cv.pdf;base64,JVBERi0xLjQK...` Two things break, and neither says so clearly: * **The `;name=` segment is mandatory.** Without it the string is no longer recognised as a file at all — the API reads it as a signed attachment id and fails with `422` and `Invalid resume content`, which looks like a corrupt upload rather than a missing filename. * **Nothing may sit between the MIME type and `;name=`.** A `charset` parameter is swallowed into the type, which becomes `application/pdf;charset=utf-8`, matches nothing in the list below, and fails with `file type invalid`. Accepted MIME types: | Format | MIME type | | ------------------- | ------------------------------------------------------------------------- | | PDF | `application/pdf` | | Word (.docx) | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | | Word (.doc) | `application/msword` | | OpenDocument (.odt) | `application/vnd.oasis.opendocument.text` | | RTF | `application/rtf`, `text/rtf`, `application/x-rtf`, `text/richtext` | The check reads the MIME type and the filename you declared — it does not look at the bytes — and the two have to agree. `data:application/pdf;name=cv.png` fails with `file type invalid` even though the type is an accepted one, so send the real type and the real extension. Enforce the same allowlist and a size limit on your own upload endpoint, so the candidate gets an immediate error instead of a round trip. PeopleForce parses the uploaded CV and enriches the candidate profile from it. For extra documents (portfolio, certificates) after the candidate exists, use [`POST /api/public/v3/recruitment/candidates/{candidate_id}/documents`](/company/v3/reference/candidates/create-recruitment-candidate-document) — a `multipart/form-data` upload in an `attachment` field. ### Custom fields [#custom-fields] Reference: [List candidate fields](/company/v3/reference/candidates/list-recruitment-candidate-fields) Fetch the custom candidate fields once, cache them, and render matching inputs on your form: ```http GET https://app.peopleforce.io/api/public/v3/recruitment/candidate_fields ``` ```json { "data": [ { "id": 41, "name": "Notice period", "internal_name": "notice_period", "type": "ApplicantFields::Select", "options": [ { "id": 501, "value": "Immediately" }, { "id": 502, "value": "1 month" } ] } ] } ``` Send them as **top-level keys** named by `internal_name`, not nested under a `custom_fields` object: ```json { "full_name": "Ada Lovelace", "email": "ada@example.com", "notice_period": 502 } ``` On `/api/public/v3`, select and multi-select fields take the option **`id`** (`502`, or `[502, 503]`). Other types take their plain value: text as a string, checkbox as a boolean, date as `YYYY-MM-DD`. A field marked required in PeopleForce is enforced here. Omitting it fails the whole candidate creation with `422`, so mirror the requirement on your form. ### Recording consent [#recording-consent] Hosting the form yourself means you own the GDPR consent checkboxes — PeopleForce cannot render them for you. Pass the timestamps through: * `consented_at` — consent to process the data for **this** application. * `future_recruitment_consented_at` — consent to stay on file for **future** vacancies. Both are ISO 8601 (`"2026-08-16T10:32:00Z"`). Send `future_recruitment_consented_at` only if that separate box was ticked — it drives data retention in PeopleForce. ### Tracking the source [#tracking-the-source] Reference: [List sources](/company/v3/reference/candidates/list-recruitment-sources) Candidates created through the API have no source unless you set one, which leaves them out of source-effectiveness reporting. Fetch the list once and cache the id: ```http GET https://app.peopleforce.io/api/public/v3/recruitment/sources ``` Pick the source for your site, or create one with `POST /api/public/v3/recruitment/sources` (`{"name": "Company website"}`), and pass its id as `source_id` on the candidate. If your career page also lands paid campaigns, keep reading the `source_id` query parameter you already use for the hosted form and map it to the matching PeopleForce source. ### Handling duplicates [#handling-duplicates] PeopleForce blocks duplicate candidates. Step 1 returns `422` when: * the **email** already belongs to another candidate; * a **phone number** already belongs to another candidate, if *Detect duplicates by phone number* is enabled in your recruiting settings; * the **same CV file** is already attached to another candidate. ```json { "success": false, "errors": ["Email already exists for Ada Lovelace"] } ``` A returning candidate is the normal case. Look them up ([List candidates](/company/v3/reference/candidates/list-recruitment-candidates)) and reuse them: ```http GET https://app.peopleforce.io/api/public/v3/recruitment/candidates?email=ada@example.com ``` The `email` filter matches on a **prefix**, so `ada@example.co` also returns `ada@example.com`. Pick the row whose `email` equals the address exactly — never just `data[0]` — then go to step 2 with its `id`. If that person has **already applied to this vacancy**, step 2 returns `422` with `"Applicant has already been taken"`. They are already in the pipeline, so show "you have already applied to this role" rather than an error. To refresh their CV and contact details with what they just submitted, call [`PUT /api/public/v3/recruitment/candidates/{id}`](/company/v3/reference/candidates/update-recruitment-candidate) before step 2. Looking a candidate up by **phone** instead? That filter takes a list, so it needs brackets in the query string: `?phone_numbers[]=%2B441234567890`. Without them the value arrives as a single string and the request fails with `400`. This applies to query strings only — in the JSON bodies above, a list is just a JSON array. ### Handling failures [#handling-failures] | Status | Meaning | What to do | | ------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `400` | A parameter has the wrong type | Read `errors` — each entry names the parameter and the type it expected | | `401` | Missing, wrong, or disabled API key | Check the `X-API-KEY` header. Do not retry. | | `403` | Key lacks permission, or its IP allowlist excludes your server | Add your server's egress IP to the key's allowlist. | | `404` | Vacancy is not published, or the candidate id is wrong | Refresh your vacancy list. | | `422` | Duplicate candidate, already applied, bad file type, or missing required custom field | Resolve as described above. Do not retry unchanged. | | `429` | Rate limited | Back off and retry, honouring `Retry-After`. | | `5xx` | Temporary server error | Retry with exponential backoff. | The dangerous state is **step 1 succeeded and step 2 failed**: a candidate with no application. Do not re-create the candidate on retry — retry step 2 with the id you already hold, and log that id so a failure outliving the request can be reconciled later. ### Rate limits [#rate-limits] The API follows the usual [rate limit](/company/v3/api-basics/rate-limit) rules — 300 requests a minute per API key. Honour `Retry-After` on a `429` and queue submissions instead of retrying in a tight loop. ### Security checklist [#security-checklist] Your form is a publicly writable endpoint into your recruiting database. The protections the hosted form provides are now yours to build: * **Keep the Company API key server-side.** Never proxy it to the browser, and never forward whatever the browser sends. * **Add bot protection** — reCAPTCHA, Turnstile, or equivalent — verified on your server before the PeopleForce calls. * **Rate-limit per visitor IP** on your own endpoint, independently of the PeopleForce limit. * **Validate uploads before forwarding**: extension, MIME type, maximum size. * **Never echo PeopleForce error messages verbatim.** A duplicate error is HTML and contains the existing candidate's name, linked to their profile, which discloses that they applied to you. * **Restrict the key by IP** in PeopleForce settings, to your server's egress addresses. ### Complete example [#complete-example] ```javascript // POST /apply const PF = "https://app.peopleforce.io/api/public/v3"; const headers = { "X-API-KEY": process.env.PEOPLEFORCE_API_KEY, "Content-Type": "application/json", }; async function findCandidateByEmail(email) { // Email is optional on the form, and a duplicate can be raised on phone or // CV instead — so this is reachable with nothing to look up. if (!email?.trim()) return null; const res = await fetch(`${PF}/recruitment/candidates?email=${encodeURIComponent(email)}`, { headers }); const body = await res.json(); // The filter is a prefix match, so "ada@example.co" also returns // "ada@example.com". Take the exact address, never just the first row. const wanted = email.trim().toLowerCase(); return body.data?.find((c) => c.email?.toLowerCase() === wanted)?.id ?? null; } async function createCandidate(form, file) { const payload = { full_name: form.fullName, email: form.email, phone_numbers: form.phone ? [form.phone] : [], cover_letter: form.coverLetter, source_id: Number(process.env.PEOPLEFORCE_SOURCE_ID), consented_at: form.consentedAt, // when the box was ticked notice_period: form.noticePeriodOptionId, // custom field, by internal_name }; if (file) { payload.resume = `data:${file.mimetype};name=${file.originalname};base64,${file.buffer.toString("base64")}`; } const res = await fetch(`${PF}/recruitment/candidates`, { method: "POST", headers, body: JSON.stringify(payload), }); if (res.status === 201) return (await res.json()).data.id; if (res.status === 422) { const { errors = [] } = await res.json(); // 422 is also a bad file type or a missing required custom field, so only // a duplicate is safe to recover from. The message is "… already exists // for " for email, phone and CV alike. if (!errors.some((e) => /already exists/i.test(e))) throw new ValidationError(errors); const existingId = await findCandidateByEmail(form.email); if (existingId) return existingId; // Duplicate phone or CV rather than email — the person exists, but not at // an address we can resolve them by. throw new ValidationError(errors); } throw new Error(`Candidate creation failed: ${res.status}`); } async function createApplication(vacancyId, candidateId) { const res = await fetch(`${PF}/recruitment/vacancies/${vacancyId}/applications`, { method: "POST", headers, body: JSON.stringify({ applicant_id: candidateId, perform_automations: true, }), }); if (res.status === 422) { // The only 422 this endpoint returns is the one-application-per-vacancy // rule — but check, rather than swallowing every validation error as // "already applied". const { errors = [] } = await res.json(); if (errors.some((e) => /already been taken/i.test(e))) return { alreadyApplied: true }; throw new ValidationError(errors); } if (res.status !== 201) { throw new Error(`Application failed for candidate ${candidateId}: ${res.status}`); } return (await res.json()).data; } app.post("/apply", upload.single("resume"), async (req, res) => { if (!(await verifyCaptcha(req.body.captchaToken))) { return res.status(400).render("apply", { error: "Please confirm you are not a robot." }); } let candidateId; try { candidateId = await createCandidate(req.body, req.file); const result = await createApplication(req.body.vacancyId, candidateId); if (result.alreadyApplied) return res.render("already-applied"); res.render("thank-you"); } catch (err) { // candidateId set but the application failed is the state worth logging: // a candidate with no application, to reconcile later. logger.error({ err, candidateId }, "career application submission failed"); if (err instanceof ValidationError) { // Your own wording — never PeopleForce's, which is HTML and can name an // existing candidate. return res.status(400).render("apply", { error: "Please check your details and try again." }); } res.status(500).render("apply", { error: "We could not submit your application. Please try again." }); } }); ``` The same calls with `curl`: ```bash # 1. Create the candidate curl -X POST https://app.peopleforce.io/api/public/v3/recruitment/candidates \ -H "X-API-KEY: $PEOPLEFORCE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "full_name": "Ada Lovelace", "email": "ada@example.com", "phone_numbers": ["+44 12 3456 6789"], "consented_at": "2026-08-16T10:32:00Z", "resume": "data:application/pdf;name=ada-cv.pdf;base64,JVBERi0xLjQK..." }' # 2. Attach them to the vacancy curl -X POST https://app.peopleforce.io/api/public/v3/recruitment/vacancies/1234/applications \ -H "X-API-KEY: $PEOPLEFORCE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "applicant_id": 84213, "perform_automations": true }' ``` ## Which approach should you choose? [#which-approach-should-you-choose] | | Link to `apply_url` | Your own form + API | | ----------------------------------------- | ------------------------------- | ----------------------------------------------- | | Effort | A hyperlink | A backend endpoint, file handling, error states | | API key | Career API key (publishable) | Company API key (**server-side only**) | | Candidate stays on your domain | No | Yes | | Bot protection | Built in | You build it | | File upload and validation | Built in | You build it | | GDPR consent UI | Built in, follows your settings | You build it | | Recruiter notification on new application | Yes | Webhook only | # Changelog (/company/v1/api-basics/changelog) This log records notable changes to the Company API — newest first. ## August 16, 2026 [#august-16-2026] * Candidate CV/resume download URLs (`resume_url`) are now short-lived, expiring signed URLs (60 minutes) instead of permanent, unauthenticated links. Fetch a fresh URL before each download rather than caching it. ## June 29, 2026 [#june-29-2026] * Added `created_at`/`updated_at` filters to [list vacancy applications](https://developer.peopleforce.io/reference/list-recruitment-candidates-applications). * Added the `overtime_request_approve` webhook topic, triggered when the last approver approves an overtime request. See [Overtime requests](../webhooks/overtime-requests.mdx). ## April 14, 2024 [#april-14-2024] * Introduced **secrets** for webhooks. See [Starting with webhooks](../getting-started/starting-with-webhooks.mdx#secrets). * Webhooks can now be enabled/disabled in the web application. * Added `cover_letter` to create/update recruitment candidates. * Added custom table data to the get-employee response. * Added endpoints for time projects and project fields on timesheet entries. ## March 14, 2024 [#march-14-2024] * Added endpoints to manage the company skill list and employee skills. ## March 3, 2024 [#march-3-2024] * Added paid/unpaid and working/non-working info to leave requests. * Added endpoints for employee avatar, certifications, documents, and tables. ## December 30, 2023 [#december-30-2023] * Added bulk create/destroy for timesheet entries. ## November 26, 2023 [#november-26-2023] * Added the API rate limit. See [Rate limits](./rate-limit.mdx). ## April 2, 2023 [#april-2-2023] * Introduced **API v2**: custom fields addressed by `internal_name` (replacing UUIDs); `group` returned as an object; added an `options` field for select fields. # Pagination (/company/v1/api-basics/pagination) The API uses pagination for its list endpoints. List responses return a `data` array alongside a `metadata` object with pagination details. The page size is **50 items**. ```json { "data": [], "metadata": { "page": 1, "pages": 7, "count": 340, "items": 50 } } ``` | Field | Description | | :------ | :-------------------------------------------------------------------------------- | | `page` | The current page. | | `pages` | Total number of pages. | | `count` | Total number of items. | | `items` | Number of items on this page (50 for every page except the last, which has 1–50). | Request a specific page with the `page` query parameter: ```bash curl "https://app.peopleforce.io/api/public/v1/employees?page=2" \ -H "X-API-KEY: " ``` To walk the full result set, request `page=1` and keep incrementing `page` until it reaches `pages`. # Rate limits (/company/v1/api-basics/rate-limit) The PeopleForce API applies a rate limit to all endpoints to prevent misuse, protect the platform, and handle incoming volume. The limit is calculated **per minute** and enforced **per API key** (it falls back to the requesting IP address when a request carries no API key). The current limit is **300 requests per minute**. ## Recovering from a rate limit [#recovering-from-a-rate-limit] When you exceed the limit, the endpoint returns HTTP `429 Too Many Requests`. On a `429`, slow down your request rate and wait before retrying. Check the **`Retry-After`** response header to learn when the limit resets — its value is the number of seconds remaining until the limit is cleared. A good practice is to read that header and pause your requests for that many seconds before trying again. # Adding a new hire from another ATS (/company/v1/guides/ats-new-hire) When you hire someone in an external ATS and want them in PeopleForce without extra manual work, create them through the Company API. **Prerequisite:** an API key. If you don't have one, start with [Authentication](../getting-started/authentication.mdx). ## Creating an employee via the API [#creating-an-employee-via-the-api] An employee profile is the account an employee uses to log in. It stores their core information — name, date of birth, email, phone number, hire date, and more. Create an employee with `POST /employees`: ```bash curl -X POST https://app.peopleforce.io/api/public/v1/employees \ -H "X-API-KEY: " \ -H "Content-Type: application/json" \ -d '{ "first_name": "Andrew", "last_name": "Doe", "email": "andrew@example.com", "hired_on": "2024-07-01" }' ``` You can also provide middle name, work and personal email, hire date, position, department, division, manager ID, and any custom field. See **Create an employee** in the API reference for the full field list. ### Custom fields [#custom-fields] Address a custom field by its identifier. In **v1** custom fields are keyed by the field's **UUID**; from **v2** onward they're keyed by the generated **`internal_name`**. Look up the identifier with the **List employee fields** endpoint. ```json { "first_name": "Andrew", "tax_number": "123123" } ``` ## Troubleshooting [#troubleshooting] **Validation errors (422).** Check the response message — you may be missing a required field or sending invalid/duplicate data. Personal and work emails and the personal phone number **must be unique**; first and last name are required. If the employee already exists you'll get a `422`. Look them up by email with **List employees** (`GET /employees`), then update them with `PUT /employees/{id}`. If your ATS can't call an API directly, check whether it integrates with [Zapier](https://zapier.com/apps/peopleforce/integrations) — Zapier can sit in between, receive your new hire, and call the PeopleForce API for you. # ERP employee directory integration (/company/v1/guides/erp-directory) This guide covers the endpoints useful for pushing data into PeopleForce from an external ERP: how to set up your structural lists, create employee records, and keep them up to date. It's a starting point — for anything not covered, browse the **API reference** in the sidebar. **Prerequisite:** an API key. If you don't have one, start with [Authentication](../getting-started/authentication.mdx). ## Step 1. Prepare your structural lists [#step-1-prepare-your-structural-lists] Before creating employees, set up the org data their profiles reference. Create these in the PeopleForce UI or via the API. | Resource | Endpoint | Notes | | :--------------- | :----------------------- | :------------------------------------------------------------------------------------ | | Locations | `POST /locations` | Your offices or places of operation. | | Divisions | `POST /divisions` | Larger business units. | | Departments | `POST /departments` | Main structural units; can be nested — create parents first, then pass the parent ID. | | Employment types | `POST /employment_types` | Full-time, part-time, contractor, etc. | | Positions | `POST /positions` | Titles assigned to employees. | Already have these lists? Use the matching `GET` endpoints to fetch their IDs (which you'll need when creating employees), and the `PUT` endpoints to update them. ## Step 2. Create employees [#step-2-create-employees] ### Create the employee record [#create-the-employee-record] Create an employee with `POST /employees` — see [Adding a new hire from another ATS](./ats-new-hire.mdx) for the request shape. Fetch existing employees with `GET /employees` and update with `PUT /employees/{id}`. ### Add an employment status [#add-an-employment-status] `POST /employees/{id}/employment_statuses` records work schedule, employment type, probation, and effective date. Add a **new record** for each change to preserve history. ### Add a position [#add-a-position] `POST /employees/{id}/positions` records the employee's position, department, division, location, and manager. Add a **new record** for each change (e.g. a promotion). ### Add compensation [#add-compensation] `POST /employees/{id}/salary` records salary, frequency, currency, and effective date. Add a **new record** for each change. ## Step 3. Leave balances [#step-3-leave-balances] Receiving leave balances from an external system is **disabled by default** for security and enabled per request — [contact us](mailto:support@peopleforce.io) to set it up. The flow involves creating leave types and policies and assigning policies to employees; you can also subscribe to [leave-request webhooks](../webhooks/leave-requests.mdx) to react to changes. # Job boards integration (/company/v1/guides/job-boards) This guide is for job board platforms (e.g. Indeed-style sites where many companies post openings) that want to integrate with PeopleForce using an API key. It covers the endpoints, workflow, and best practices for syncing job postings and candidate applications. Building a careers page for a single company's own website instead? See [Own career site integration](/careers/v1/guides/own-career-site). **Endpoint availability.** This workflow relies on PeopleForce recruitment endpoints (vacancies and candidates). These are **not yet part of the public Company API v4 surface** documented here. Confirm the available endpoints and base path with the PeopleForce team before building. The workflow below is preserved from the previous integration guide. ## Integration workflow [#integration-workflow] The customer's essential tasks are: * Publish a job from PeopleForce to your platform. * Receive candidate applications and sync them back to PeopleForce. Suggested setup flow: 1. Add a settings field where customers paste their PeopleForce API key. 2. When a customer creates a job posting, let them pick from their open PeopleForce vacancies and pre-fill the details. 3. After publishing, route applicant data back to the correct PeopleForce vacancy and mark the source. ## Getting started: API key [#getting-started-api-key] 1. In PeopleForce, go to **Settings → Security → API keys**. 2. Generate a new key and store it securely. 3. See [Authentication](../getting-started/authentication.mdx) for details. One API key can be used across multiple accounts of the same customer (e.g. different departments or business units). ## Importing job data [#importing-job-data] 1. Prompt the customer to connect their account with an API key. 2. When creating a vacancy on your side, suggest selecting from existing PeopleForce vacancies. 3. Retrieve open vacancies and display those in a published state (`state=opened`). 4. Fetch the selected vacancy's details and import them (title, description, recruiter, salary, etc.). ### Vacancy data mapping [#vacancy-data-mapping] Either auto-fill your job form from the imported vacancy (requires pre-mapping fields between systems) or let customers map fields manually. ## Submitting candidates [#submitting-candidates] When a candidate applies: 1. Collect their information (name, email, phone, CV file, cover letter, etc.). 2. Send it to PeopleForce, linked to the correct vacancy. 3. Include a `source` value (e.g. your platform name) for reporting. 4. Use the applications parameter to assign the candidate to the right vacancy stage. 5. If the same candidate applies to another vacancy at the same company, update the existing candidate (by ID) and add another entry to its applications. ### Candidate de-duplication [#candidate-de-duplication] * PeopleForce auto-checks for duplicates by email and CV file. * If a duplicate is found, the candidate's information is updated. * Manual updates via the UI are also possible. ## Test environment [#test-environment] 1. Request access to the PeopleForce test environment. 2. Use test data to simulate job sync and candidate submissions. 3. When complete, schedule a live demo with the PeopleForce team and share sandbox access for QA. ## Best practices [#best-practices] * Validate the API key before initiating syncs. * Clearly mark PeopleForce-originated jobs in your interface. * Require applications via your forms (not email or redirects) to capture complete data. * Log API errors and retry failed requests automatically. For test access, technical support, or feedback, contact us via [peopleforce.io/partners](https://peopleforce.io/partners). # Using PeopleForce data in user-facing solutions (/company/v1/guides/user-facing-data) Many customers surface PeopleForce data inside user-facing solutions — for example: * Showing a list of employees * Retrieving leave requests * Showing directory data such as positions or departments * Displaying vacancies on a website When you build this, implement it carefully to avoid exposing employee data and creating a security risk. ## Showing vacancies [#showing-vacancies] For displaying vacancies on a public site, use the dedicated **Career API key** and the Careers API — see [Own career site integration](/careers/v1/guides/own-career-site). The Career key is limited to non-sensitive vacancy data, so it's safe in a browser. ## Everything else: never expose the API key [#everything-else-never-expose-the-api-key] For all other data, **never put a Company API key in the browser (frontend)**. Protect it behind a backend proxy service. This matters even for internal, non-public solutions — a key exposed to your own team is still a data-security risk. A good pattern is a **two-tier architecture**: the frontend talks only to your backend, and your backend holds the API key and calls PeopleForce. 1. The user-facing app requests data from your backend — it never references a PeopleForce API key. 2. The backend receives the request and calls PeopleForce, using the API key it holds server-side (optionally filtering by use case). 3. PeopleForce responds to the backend. 4. The backend reduces the response to only the fields the frontend needs, then returns it. This keeps the API key encapsulated in the backend, where it's never exposed to the client. Combine this with key restrictions — IP allow-lists and field-level limits on the Company API key (see [Authentication](../getting-started/authentication.mdx)) — for defence in depth. # Authentication (/company/v1/getting-started/authentication) PeopleForce authenticates API requests with an API key. There are **two types of key** depending on your use case — choose the one that matches your goal. | Key type | Access | Where it can be used | | :------------------ | :----------------------------------------------------------------------------- | :------------------------------------------------------------------------ | | **Company API key** | Full access to the PeopleForce API and people data, with minimal restrictions. | Server-to-server integrations only. **Never** on a public-facing website. | | **Career API key** | Limited to non-sensitive, vacancy-specific data. | Safe to use on a public-facing website (e.g. a custom careers page). | To create a key, go to **Settings → API keys** (at the bottom of the page) → **Generate API key**. ## Company API key [#company-api-key] A Company API key lets the holder retrieve or change data in your company account through the API. Use it to build any system integration to PeopleForce. A Company API key grants access to nearly all data in PeopleForce. Only use it in trusted server-to-server integrations, never on a public-facing website, and only share it with developers you trust. You can scope a Company key with these restrictions: * **People compensation** — limit viewing or editing of people compensation. * **Vacancy salary range** — limit viewing or editing of vacancy salary ranges. * **Candidate desired salary** — limit viewing or editing of candidate desired salary. * **Candidate sensitive fields** — limit viewing or editing of candidate sensitive fields. * **IP addresses** — restrict callers to an allow-list of IP addresses (e.g. office or home networks). Generating a Company API key ## Career API key [#career-api-key] A Career API key lets you retrieve vacancies from PeopleForce to build a custom careers page on your own website. Because it is limited to non-sensitive, vacancy-specific information, it is safe to embed in a public-facing site. See [Own career site integration](/careers/v1/guides/own-career-site) for how to use it. Generating a Career API key ## Using a key [#using-a-key] Pass the key in a request header named **`X-API-KEY`**: ```bash curl https://app.peopleforce.io/api/public/v1/employees \ -H "X-API-KEY: " ``` All API requests must be made over HTTPS — calls over plain HTTP will fail, and so will requests without authentication. Passing the API key in the X-API-KEY header ## Disabling a key [#disabling-a-key] From the API key list page you can **disable** a key to temporarily stop it from working without deleting it — useful while pausing an integration. The API keys list ## Revoking a key [#revoking-a-key] If you no longer need a key, delete it from **Settings → API keys** → find the key → **Delete**. Deleting an API key from the list Deleting an API key is permanent and immediate. Any integration using that key stops working at once, and the key cannot be recreated. Account for every integration before deleting. ## Troubleshooting [#troubleshooting] ### 401 Unauthorized [#401-unauthorized] Double-check that the API key was copied correctly and try again. ```json { "message": "Bad Credentials" } ``` ### 403 Forbidden [#403-forbidden] Your role doesn't have permission for this action — most often because the key is restricted from compensation data. ### 404 Not Found [#404-not-found] The resource could not be found. Check that your request refers to an existing object. ### 422 Unprocessable Entity [#422-unprocessable-entity] One or more fields failed validation. The response body lists every error: ```json { "success": false, "errors": [ "Field name can't be blank" ] } ``` ### 500 Internal Server Error [#500-internal-server-error] A problem on our side. Try again later, or [contact support](mailto:support@peopleforce.io). # FAQ (/company/v1/getting-started/faq) ## Is using the API free? [#is-using-the-api-free] Yes — the API is free for all PeopleForce clients, and we encourage everyone to use it. ## What are the API rate limits? [#what-are-the-api-rate-limits] The current limit is **300 requests per minute**. See [Rate limits](../api-basics/rate-limit.mdx) for details. ## I don't see Webhooks, API, or Settings in PeopleForce [#i-dont-see-webhooks-api-or-settings-in-peopleforce] These pages are only visible to admin users, or users with the webhook or API permission. Contact your PeopleForce administrator to request access. ## I found a bug / I got a 500 error [#i-found-a-bug--i-got-a-500-error] Sorry to hear it — we aim for the best quality in our product. If you hit an issue, [let us know](mailto:support@peopleforce.io) and we'll work on it. A `500` response automatically notifies our team, but you're still welcome to write in and describe what happened — it helps us resolve it faster, and we'll let you know once there's a fix. ## I couldn't find the endpoint or data I was looking for [#i-couldnt-find-the-endpoint-or-data-i-was-looking-for] We cover a wide range of cases across the API and webhooks, but maybe yours is new to us. [Let us know](mailto:support@peopleforce.io) and we'll do our best to help — see also [Request a feature](./request-a-feature.mdx). # Introduction (/company/v1/getting-started) You can use the PeopleForce Company API to access your PeopleForce data — retrieving information about the entities stored in your account and performing actions on them. The API is organised around REST: predictable resource-oriented URLs, JSON request and response bodies, and standard HTTP verbs and status codes. For example, you can sync employees into PeopleForce from an external system, keep your lists of departments, divisions, and positions up to date, read time-off balances, or pull engagement-survey results. You'll need an API key first; see [Authentication](./authentication.mdx) to generate one. Then browse the full endpoint list under **API reference** in the sidebar — every operation has a live "Send" panel. ## Explore the platform's capabilities [#explore-the-platforms-capabilities] * **[Authentication](./authentication.mdx)** — generate an API key and authenticate every request. * **[Webhooks](../webhooks/index.mdx)** — subscribe to events and get notified the moment data changes in PeopleForce. Use the version switcher at the top of the sidebar to move between API versions. This page applies to every version. If you have any issues or questions, [contact us](mailto:support@peopleforce.io). # Request a feature (/company/v1/getting-started/request-a-feature) Didn't find what you were looking for in the API or webhooks? Let us know. We keep the API broad and already cover the common use cases — but if something is missing, send us a feature request and we'll see whether it can be added. [Open the requests board →](https://feedback.peopleforce.io/b/for-devs) # Starting with webhooks (/company/v1/getting-started/starting-with-webhooks) Webhooks let external services be notified when certain events happen in PeopleForce. When a subscribed event occurs, PeopleForce sends a `POST` request to each URL you configure. ## Setting up a webhook [#setting-up-a-webhook] Go to **Settings → Webhooks → Add new webhook** and fill in four fields: * **Name** — a label so you can find the webhook later. * **Payload URL** — the server that receives the webhook `POST` requests. It must use a valid SSL certificate from a publicly trusted CA. * **Secret** — an optional string used to sign requests (see below). * **Topics** — the events you want to be notified about; you can choose multiple. Save the webhook and it's live. See the full list of events in the [Webhooks overview](../webhooks/index.mdx). Creating a new webhook in Settings ## Secrets [#secrets] A secret is a shared string used to authenticate webhook deliveries. If you set one, PeopleForce signs each request and adds an `x-peopleforce-signature` header. Without the secret, no one else can forge a request with a matching signature. To verify the signature: * It is computed with **HMAC-SHA256** over the raw request body, keyed with your secret. * The result is a hexadecimal digest, prefixed with `sha256=`. * Compare it against the `x-peopleforce-signature` header using a **constant-time** comparison to avoid timing attacks. * Treat the payload as UTF-8 encoded text. ```ruby title="Ruby" def verify_signature(payload_body, signature_header) secret_key = ENV['WEBHOOK_SECRET'] computed_signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret_key, payload_body) unless Rack::Utils.secure_compare(computed_signature, signature_header) return halt 500, "Signatures didn't match!" end end ``` ```python title="Python" import hmac import hashlib import os def verify_signature(payload_body, signature_header): secret_token = os.getenv('WEBHOOK_SECRET') if not signature_header: raise HTTPException(status_code=403, detail="Missing signature header!") hash_object = hmac.new(key=secret_token.encode('utf-8'), msg=payload_body.encode('utf-8'), digestmod=hashlib.sha256) expected_signature = 'sha256=' + hash_object.hexdigest() if not hmac.compare_digest(expected_signature, signature_header): raise HTTPException(status_code=403, detail="Signatures did not match!") ``` ## Testing a webhook [#testing-a-webhook] When an event fires, PeopleForce delivers the JSON payload as the body of the `POST` request. To try it out, point the Payload URL at a request inspector such as [webhook.site](https://webhook.site/) (external link — be mindful of your data), then trigger the event. For a new employee, the payload looks like: ```json { "action": "employee_create", "data": { "id": 130333, "attributes": { "employee_number": "PF124593", "hired_on": "2022-09-12", "probation_ends_on": "2022-12-12", "first_name": "John", "last_name": "Doe", "email": "john@peopleforce.io", "personal_email": null, "gender": "male", "mobile_number": "", "work_phone_number": "", "date_of_birth": "1978-08-31", "termination_effective_date": null, "termination_comment": null, "avatar_url": null }, "reporting_to": { "id": 5844, "full_name": "Ross Kate", "email": "kate@peopleforce.io" }, "employment_type": { "id": 1899, "name": "Full-Time" }, "position": { "id": 11943, "name": "Senior Developer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": null, "custom_fields": { "2692d87e-c388-49ea-b903-616bc1557746": { "name": "T-shirt size", "value": "M", "group": "Personal" } }, "meta": { "created_at": "2022-09-05T18:33:55.130+03:00", "updated_at": "2022-09-05T18:33:55.526+03:00" } } } ``` Webhook payloads are emitted by the PeopleForce platform and use the platform's own field names — they are independent of the REST API version you call. ## Execution history [#execution-history] To confirm a delivery, go to **Settings → Webhooks** and click a webhook's name to see its execution history. Click any delivery to view the raw data that was sent. Webhook execution history ## Troubleshooting [#troubleshooting] If a delivery didn't arrive or didn't look right: 1. Check the Payload URL is correct and the receiving server supports webhooks. 2. Check the webhook's run history. If it was delivered, re-check step 1, then step 3. 3. Make sure your receiving server is up and healthy. | Status | Meaning | | :---------------------------- | :------------------------------------------------------------------------------------------- | | **200 Success** | Webhook was delivered. | | **404 Not Found** | The resource could not be found — check it refers to an existing object. | | **500 Internal Server Error** | A problem on our side — try again later or [contact support](mailto:support@peopleforce.io). | If none of these help, [contact us](mailto:support@peopleforce.io). # Changelog (/company/v2/api-basics/changelog) This log records notable changes to the Company API — newest first. ## August 16, 2026 [#august-16-2026] * Candidate CV/resume download URLs (`resume_url`) are now short-lived, expiring signed URLs (60 minutes) instead of permanent, unauthenticated links. Fetch a fresh URL before each download rather than caching it. ## June 29, 2026 [#june-29-2026] * Added `created_at`/`updated_at` filters to [list vacancy applications](https://developer.peopleforce.io/reference/list-recruitment-candidates-applications). * Added the `overtime_request_approve` webhook topic, triggered when the last approver approves an overtime request. See [Overtime requests](../webhooks/overtime-requests.mdx). ## April 14, 2024 [#april-14-2024] * Introduced **secrets** for webhooks. See [Starting with webhooks](../getting-started/starting-with-webhooks.mdx#secrets). * Webhooks can now be enabled/disabled in the web application. * Added `cover_letter` to create/update recruitment candidates. * Added custom table data to the get-employee response. * Added endpoints for time projects and project fields on timesheet entries. ## March 14, 2024 [#march-14-2024] * Added endpoints to manage the company skill list and employee skills. ## March 3, 2024 [#march-3-2024] * Added paid/unpaid and working/non-working info to leave requests. * Added endpoints for employee avatar, certifications, documents, and tables. ## December 30, 2023 [#december-30-2023] * Added bulk create/destroy for timesheet entries. ## November 26, 2023 [#november-26-2023] * Added the API rate limit. See [Rate limits](./rate-limit.mdx). ## April 2, 2023 [#april-2-2023] * Introduced **API v2**: custom fields addressed by `internal_name` (replacing UUIDs); `group` returned as an object; added an `options` field for select fields. # Pagination (/company/v2/api-basics/pagination) The API uses pagination for its list endpoints. List responses return a `data` array alongside a `metadata` object with pagination details. The page size is **50 items**. ```json { "data": [], "metadata": { "page": 1, "pages": 7, "count": 340, "items": 50 } } ``` | Field | Description | | :------ | :-------------------------------------------------------------------------------- | | `page` | The current page. | | `pages` | Total number of pages. | | `count` | Total number of items. | | `items` | Number of items on this page (50 for every page except the last, which has 1–50). | Request a specific page with the `page` query parameter: ```bash curl "https://app.peopleforce.io/api/public/v2/employees?page=2" \ -H "X-API-KEY: " ``` To walk the full result set, request `page=1` and keep incrementing `page` until it reaches `pages`. # Rate limits (/company/v2/api-basics/rate-limit) The PeopleForce API applies a rate limit to all endpoints to prevent misuse, protect the platform, and handle incoming volume. The limit is calculated **per minute** and enforced **per API key** (it falls back to the requesting IP address when a request carries no API key). The current limit is **300 requests per minute**. ## Recovering from a rate limit [#recovering-from-a-rate-limit] When you exceed the limit, the endpoint returns HTTP `429 Too Many Requests`. On a `429`, slow down your request rate and wait before retrying. Check the **`Retry-After`** response header to learn when the limit resets — its value is the number of seconds remaining until the limit is cleared. A good practice is to read that header and pause your requests for that many seconds before trying again. # Candidates and vacancies (/company/v1/webhooks/candidates-and-vacancies) For PeopleRecruit users, these webhooks cover the recruitment lifecycle: * `applicant_create` — a candidate is created * `vacancy_offer_accept` — a candidate accepted and signed an offer * `vacancy_create` — a vacancy is created * `vacancy_application_create` — a candidate applied or was added to a vacancy ## Applicant created [#applicant-created] Triggered instantly when a new applicant is created. Action: `applicant_create`. Custom fields are keyed by their field ID, each with a `name` and `value`. ```json { "action": "applicant_create", "data": { "id": 1461719, "attributes": { "full_name": "Pavlo Skrypka", "position": null, "email": "pavlo@example.com", "phone_numbers": ["380948593490"], "urls": [] }, "custom_fields": { "70f2c0d7-559a-4066-be3a-1cc53e547c52": { "name": "Driving license", "value": null }, "c162e147-7c10-4bee-b53b-72f0183edbc2": { "name": "Special notes", "value": null }, "18700c50-a0ac-40e1-8e52-f636a64a3edc": { "name": "desired_salary", "value": null } }, "meta": { "created_at": "2022-09-21T16:17:10.410+01:00", "updated_at": "2022-09-21T16:17:10.416+01:00" } } } ``` ## Applicant offer accepted [#applicant-offer-accepted] Triggered instantly when an applicant accepts and signs an offer. Action: `vacancy_offer_accept`. The payload does not include the offer document. ```json { "action": "vacancy_offer_accept", "data": { "id": 7978, "attributes": { "accepted_at": "2022-09-21T16:20:21.035+01:00", "rejected_at": null, "viewed_at": "2022-09-21T16:20:17.229+01:00" }, "applicant": { "id": 1461719, "full_name": "Pavlo Skrypka", "email": "pavlo@example.io" }, "vacancy": { "id": 526, "title": "QA Tester", "status": "accepted" }, "meta": { "created_at": "2022-09-21T16:19:58.757+01:00", "updated_at": "2022-09-21T16:20:21.013+01:00" } } } ``` ## Vacancy created [#vacancy-created] Triggered instantly when a vacancy is created. Action: `vacancy_create`. The `description` is delivered as HTML. ```json { "action": "vacancy_create", "data": { "id": 56721, "attributes": { "name": "UX Designer", "description": "
" }, "custom_fields": { "478214e1-0b81-4048-a6a3-63d3088d1ee0": { "name": "Custom field", "value": "Custom text" } }, "meta": { "created_at": "2024-02-01T16:20:40.896+00:00", "updated_at": "2024-02-01T16:20:40.906+00:00" } } } ``` ## Vacancy application created [#vacancy-application-created] Triggered instantly when a candidate applies or is added to a vacancy. Action: `vacancy_application_create`. ```json { "action": "vacancy_application_create", "data": { "id": 3102839, "candidate": { "id": 2151205, "full_name": "Jasmin Doe", "email": null }, "vacancy": { "id": 56721, "title": "UX Designer" }, "meta": { "created_at": "2024-02-01T16:27:14.814+00:00", "updated_at": "2024-02-01T16:27:14.814+00:00" } } } ``` # Employee compensation (/company/v1/webhooks/employee-compensation) Alongside an employee's profile attributes, PeopleForce keeps separate records for **position** and **salary**. To preserve the history of salary reviews and other changes, add a new record rather than editing the existing one. The salary record emits these webhooks: * `employee_salary_create` — a salary record is added * `employee_salary_update` — a salary record is updated Both fire immediately for any employee, active or terminated. ## Employee salary created [#employee-salary-created] Action: `employee_salary_create`. ```json { "action": "employee_salary_create", "data": { "id": 80093, "attributes": { "effective_on": "2022-09-21", "amount": "45000.0", "per": "month", "currency_code": "UAH", "comment": "" }, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com" }, "meta": { "created_at": "2022-09-21T15:59:57.173+01:00", "updated_at": "2022-09-21T15:59:57.173+01:00" } } } ``` ## Employee salary updated [#employee-salary-updated] Action: `employee_salary_update`. The payload shape matches `employee_salary_create`, with the updated values. # Employee job profile (/company/v1/webhooks/employee-job-profile) Alongside an employee's profile attributes, PeopleForce keeps a **job profile** history for each employee. As with position and salary, add a new record rather than editing the existing one so the history of moves is preserved. The job profile record emits these webhooks: * `employee_job_profile_create` — a job profile record is added * `employee_job_profile_update` — a job profile record is updated Both fire immediately for any employee, active or terminated. ## When they fire [#when-they-fire] A delivery is sent when the record is saved through any of these paths: * the **Job profile** section of an employee's profile * the Public API (`POST` / `PATCH` on an employee's job profiles) * an approved employee change request that adds a job profile record — this emits `employee_job_profile_create` * compliance workforce-planning verification, which adds a record `employee_job_profile_update` fires only when the save changes at least one stored value. A request that submits identical values sends nothing. A failed save sends nothing. ## Employee job profile created [#employee-job-profile-created] Action: `employee_job_profile_create`. ```json { "action": "employee_job_profile_create", "data": { "id": 4, "attributes": { "effective_on": "2026-08-14" }, "employee": { "id": 6, "employee_number": "IT-1029", "first_name": "Charles", "last_name": "Lancaster", "email": "charles@example.com" }, "job_profile": { "id": 2, "name": "Senior Developer" }, "job_group": { "id": 1, "name": "Engineering" }, "meta": { "created_at": "2026-08-14T14:18:40.723Z", "updated_at": "2026-08-14T14:18:40.723Z" } } } ``` ## Employee job profile updated [#employee-job-profile-updated] Action: `employee_job_profile_update`. The payload shape matches `employee_job_profile_create`, with the updated values. ```json { "action": "employee_job_profile_update", "data": { "id": 2, "attributes": { "effective_on": "2026-08-01" }, "employee": { "id": 4, "employee_number": null, "first_name": "Alex", "last_name": "Johnstone", "email": "alex@example.com" }, "job_profile": { "id": 1, "name": "Developer" }, "job_group": { "id": 1, "name": "Engineering" }, "meta": { "created_at": "2026-08-14T14:15:40.159Z", "updated_at": "2026-08-14T14:18:09.037Z" } } } ``` ## Field reference [#field-reference] | Field | Type | Notes | | :----------------------------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------- | | `data.id` | integer | The job profile history record ID, not the job profile ID. | | `data.attributes.effective_on` | date | The date the record takes effect. | | `data.employee` | object | Employee identity: `id`, `employee_number`, `first_name`, `last_name`, and `email`. `employee_number` is `null` when unset. | | `data.job_profile` | object | The assigned job profile: `id` and `name`. | | `data.job_group` | object or null | The job group the profile belongs to: `id` and `name`. `null` if the profile has no job group. | | `data.meta` | object | `created_at` and `updated_at` for the history record. | # Employee position (/company/v1/webhooks/employee-position) Alongside an employee's profile attributes (name, hire date, and so on), PeopleForce keeps separate records for **position** and **salary**. To preserve the history of promotions and other changes, add a new record rather than editing the existing one. The position record emits these webhooks: * `employee_position_create` — a position record is added * `employee_position_update` — a position record is updated Both fire immediately for any employee, active or terminated. ## Employee position created [#employee-position-created] Action: `employee_position_create`. ```json { "action": "employee_position_create", "data": { "id": 224222, "attributes": { "effective_on": "2022-12-01" }, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com" }, "reporting_to": { "id": 10277, "first_name": "Pahney", "last_name": "Zhelezo" }, "position": { "id": 11943, "name": "Senior Developer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": { "id": 28693, "name": "Vinnytsia" }, "meta": { "created_at": "2022-09-21T14:57:29.151+01:00", "updated_at": "2022-09-21T14:57:29.151+01:00" } } } ``` ## Employee position updated [#employee-position-updated] Action: `employee_position_update`. The payload shape matches `employee_position_create`, with the updated values. # Employee profile (/company/v1/webhooks/employee-profile) An employee profile is the account of an employee. Employees may log in using their email or Active Directory username, so take care when changing those fields. The employee profile emits these webhooks: * `employee_create` — an employee profile is created * `employee_update` — an employee profile is updated * `employee_start` — an employee's first day * `employee_terminate` — an employee is terminated * `employee_termination_revert` — a termination is reverted (reactivation) Webhook payloads are emitted by the PeopleForce platform and use the platform's own field names (e.g. `position`, `employee_number`) — independent of the Company API v4 resource naming. ## Employee profile created [#employee-profile-created] Triggered instantly when a new employee profile is created. Action: `employee_create`. ```json { "action": "employee_create", "data": { "id": 133710, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-09-26", "probation_ends_on": "2022-12-26", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com", "personal_email": null, "gender": "male", "mobile_number": "", "work_phone_number": "", "date_of_birth": "1978-08-29", "termination_effective_date": null, "termination_comment": null, "avatar_url": null }, "reporting_to": { "id": 10277, "full_name": "Zhelezo Pahney", "email": "zp@peopleforce.io" }, "employment_type": { "id": 1899, "name": "Full-Time" }, "position": { "id": 114651, "name": "IT Support Engineer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": { "id": 28693, "name": "Vinnytsia" }, "custom_fields": { "a25b06fb-bcfb-4aac-b695-6aff138fac36": { "name": "HR manager", "value": "5844", "group": "Personal" } }, "meta": { "created_at": "2022-09-21T16:29:07.894+03:00", "updated_at": "2022-09-21T16:29:08.713+03:00" } } } ``` ## Employee profile updated [#employee-profile-updated] Triggered instantly when an existing employee profile is updated. Action: `employee_update`. The payload shape matches `employee_create`, with the updated field values. ## Employee first day [#employee-first-day] Triggered in two cases: 1. Instantly, if an employee is created with a hire date of today. 2. Every night at 01:00 (UTC) for all employees whose hire date is that day. Action: `employee_start`. The payload shape matches `employee_create`. ```json { "action": "employee_start", "data": { "id": 133710, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-09-12", "first_name": "John", "last_name": "Doe", "email": "john@peopleforce.io" }, "meta": { "created_at": "2022-09-05T15:33:55.130Z", "updated_at": "2022-09-05T15:33:55.526Z" } } } ``` ## Employee terminated [#employee-terminated] Triggered in two cases: 1. Instantly, if an employee is terminated with a past date. 2. Every night at 01:00 (UTC) for all employees whose termination date is that day. Action: `employee_terminate`. The payload shape matches `employee_create`, with `termination_effective_date` set and most relational fields `null`. ```json { "action": "employee_terminate", "data": { "id": 123512, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-07-26", "first_name": "John", "last_name": "Doe", "termination_effective_date": "2022-11-07", "termination_comment": "" }, "reporting_to": null, "position": null, "department": null, "division": null, "location": null, "meta": { "created_at": "2022-07-29T10:42:43.354+01:00", "updated_at": "2022-11-10T11:12:32.102+00:00" } } } ``` ## Termination reverted [#termination-reverted] Triggered when an employee's termination is reverted — either cancelled before the termination date, or a terminated employee is reactivated. Action: `employee_termination_revert`. The payload shape matches `employee_create`. There is no `employee_activate` webhook. Reactivation and un-termination are delivered as `employee_termination_revert`. # Employee custom table rows (/company/v1/webhooks/employee-table-rows) **Custom tables** are the repeating tables you define yourself on an employee's profile — certifications, equipment, dependants, and so on. Each row emits its own event, so an integration can stay in step without polling. The custom table row emits these webhooks: * `employee_table_row_create` — a row is added * `employee_table_row_update` — a row is changed * `employee_table_row_destroy` — a row is deleted Subscribe to each one independently. Selecting one does not subscribe you to the other two, and existing endpoints gain none of them until you add them. Every payload is self-contained: it carries the employee, the table, and every column with its current value, so you do not need a follow-up API call. Values are the current state only — there is no before-and-after comparison. ## When they fire [#when-they-fire] A delivery is sent when the row is saved or deleted through any of these paths: * the employee's profile * the Public API (`POST` / `PATCH` / `DELETE` on an employee's table rows) * an approved employee change request that adds a row — this emits `employee_table_row_create` One event is sent per changed row, so a change request touching three rows sends three deliveries. ### When they do not fire [#when-they-do-not-fire] These topics cover **custom** tables only. Rows in PeopleForce system tables — position, salary, employment history, and compliance tables — never emit them. Nothing is sent when: * the save changes no stored value, so an identical update is silent * the create, update, delete, or approval fails * a change request is submitted, rejected, or cancelled — only approval emits * a row is created by a **hire form or preboarding form** ## Employee custom table row created [#employee-custom-table-row-created] Action: `employee_table_row_create`. ```json { "action": "employee_table_row_create", "data": { "id": 88213, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" }, "table": { "id": 4471, "name": "Certifications", "internal_name": "certifications" }, "columns": [ { "id": 90114, "name": "Level", "internal_name": "level", "type": "EmployeeTableColumns::Text", "value": "Professional" }, { "id": 90115, "name": "Certified on", "internal_name": "certified_on", "type": "EmployeeTableColumns::Date", "value": null }, { "id": 90116, "name": "Score", "internal_name": "score", "type": "EmployeeTableColumns::Number", "value": "87" }, { "id": 90117, "name": "Renewable", "internal_name": "renewable", "type": "EmployeeTableColumns::CheckBox", "value": "1" }, { "id": 90118, "name": "Rating", "internal_name": "rating", "type": "EmployeeTableColumns::SingleSelect", "value": { "id": "3312", "name": "Excellent" } }, { "id": 90119, "name": "Topics", "internal_name": "topics", "type": "EmployeeTableColumns::MultipleSelect", "value": [ { "id": "3315", "name": "Ruby" }, { "id": "3316", "name": "Rails" } ] }, { "id": 90120, "name": "Mentor", "internal_name": "mentor", "type": "EmployeeTableColumns::Reference", "value": { "id": "133711", "name": "Grace Hopper" } }, { "id": 90121, "name": "Notes", "internal_name": "notes", "type": "EmployeeTableColumns::LongText", "value": "Renewal due next year" } ], "meta": { "created_at": "2026-08-17T09:41:12.204Z", "updated_at": "2026-08-17T09:41:12.204Z" } } } ``` ## Employee custom table row updated [#employee-custom-table-row-updated] Action: `employee_table_row_update`. The payload shape matches `employee_table_row_create`, with the row's values after the change. ## Employee custom table row deleted [#employee-custom-table-row-deleted] Action: `employee_table_row_destroy`. The payload shape matches `employee_table_row_create`, and carries the row's final values as they stood before deletion. ## Field reference [#field-reference] | Field | Type | Notes | | :----------------------------- | :------ | :------------------------------------------------------------------------------------------ | | `data.id` | integer | The row ID. | | `data.employee` | object | Employee identity: `id`, `employee_number`, `first_name`, `last_name`, and `email`. | | `data.table` | object | The custom table: `id`, `name`, and `internal_name`. | | `data.columns` | array | Every column on the table, in the order shown in the product. Deleted columns are excluded. | | `data.columns[].id` | integer | The column ID. | | `data.columns[].internal_name` | string | The stable API name. Prefer it over `name`, which admins can rename freely. | | `data.columns[].type` | string | The column type — see the table below. | | `data.columns[].value` | varies | The current value — see the table below. | | `data.meta` | object | `created_at` and `updated_at` for the row. | ### Column values by type [#column-values-by-type] Every column on the table appears in `columns`, whether or not it holds a value. An empty column is present with a `null` value. | `type` | `value` shape | Empty | | :------------------------------------- | :------------------------------------------ | :----- | | `EmployeeTableColumns::Text` | string | `null` | | `EmployeeTableColumns::Number` | string, e.g. `"87"` | `null` | | `EmployeeTableColumns::Date` | string, e.g. `"2026-08-14"` | `null` | | `EmployeeTableColumns::CheckBox` | string, `"1"` when ticked or `"0"` when not | `null` | | `EmployeeTableColumns::LongText` | string, HTML stripped to plain text | `null` | | `EmployeeTableColumns::SingleSelect` | `{ "id": "…", "name": "…" }` | `null` | | `EmployeeTableColumns::MultipleSelect` | array of `{ "id": "…", "name": "…" }` | `[]` | | `EmployeeTableColumns::Reference` | `{ "id": "…", "name": "…" }` — an employee | `null` | Four details to code against: * **Scalars are JSON strings, not JSON numbers or booleans.** A number column sends `"87"`, a date sends `"2026-08-14"`, and a checkbox sends `"1"` or `"0"`. Both write paths store the value as text and the payload returns it uncast, so parse rather than assume. Rows loaded by an older import may still hold a raw JSON number. * An **unticked** checkbox sends `"0"`, which is a real value, not an empty one. Only a checkbox that was never set sends `null`. * A multi-select is **always an array**, never `null`. An empty one is `[]`. * The `id` inside a select, multi-select, or reference value is a **string**, because that is how the row stores it. The surrounding `columns[].id` and `data.id` are integers. A reference value keeps its stored `id` even when the referenced employee can no longer be found, in which case `name` is `null`. ## Testing a payload [#testing-a-payload] The sample-payload endpoint (`GET /webhooks?key=`) does not yet cover the three custom table row topics and returns `204 No Content` for them. To see a real payload, subscribe an endpoint, change a row, then open the delivery in **Settings → Webhooks → delivery history**. # Webhooks overview (/company/v1/webhooks) Webhooks let you subscribe to events happening in PeopleForce. Rather than polling the API, you configure an endpoint and PeopleForce sends it an HTTP `POST` request whenever a subscribed event fires. You manage subscriptions — and review past deliveries and their payloads — in your PeopleForce **Settings**. See [Starting with webhooks](../getting-started/starting-with-webhooks.mdx) for how to create one, secure it with a signing secret, and test deliveries. Every delivery has the shape `{ "action": "", "data": { … } }`, where `action` is the topic name (e.g. `employee_create`). Doesn't your integration support receiving webhooks? Check whether it supports Zapier — PeopleForce offers a range of [triggers and actions for Zapier](https://zapier.com/apps/peopleforce/integrations). ## Available topics [#available-topics] PeopleForce offers the webhook topics below. Availability depends on the PeopleForce modules enabled for your account. ### Employee [#employee] | Topic | Description | | :---------------------------------------- | :---------------------------------------------------- | | `employee_create` | An employee is created. | | `employee_update` | An employee is updated. | | `employee_start` | An employee's first day (hire date is reached). | | `employee_terminate` | An employee is terminated. | | `employee_termination_revert` | An employee's termination is reverted (reactivation). | | `employee_position_create` | An employee position record is created. | | `employee_position_update` | An employee position record is updated. | | `employee_job_profile_create` | An employee job profile record is created. | | `employee_job_profile_update` | An employee job profile record is updated. | | `employee_table_row_create` | A row in an employee's custom table is created. | | `employee_table_row_update` | A row in an employee's custom table is updated. | | `employee_table_row_destroy` | A row in an employee's custom table is deleted. | | `employee_salary_create` | An employee salary record is created. | | `employee_salary_update` | An employee salary record is updated. | | `employee_additional_compensation_create` | An additional compensation is created. | | `employee_additional_compensation_update` | An additional compensation is updated. | | `employee_employment_status_create` | An employment status record is created. | | `employee_employment_status_update` | An employment status record is updated. | | `external_user_create` | An external user is created. | ### Leave [#leave] | Topic | Description | | :----------------------- | :---------------------------- | | `leave_request_create` | A leave request is created. | | `leave_request_approve` | A leave request is approved. | | `leave_request_reject` | A leave request is rejected. | | `leave_request_withdraw` | A leave request is withdrawn. | | `leave_request_destroy` | A leave request is deleted. | ### Recruitment [#recruitment] | Topic | Description | | :----------------------------- | :--------------------------------------------------------------------- | | `applicant_create` | A candidate is created. | | `applicant_destroy` | A candidate is deleted. | | `vacancy_create` | A vacancy is created. | | `vacancy_application_create` | A candidate applied or was added to a vacancy. | | `vacancy_application_movement` | An application moved pipeline stage, was disqualified, or requalified. | | `vacancy_offer_accept` | A vacancy offer was accepted by the candidate. | | `vacancy_offer_reject` | A vacancy offer was rejected by the candidate. | ### Time [#time] | Topic | Description | | :------------------------- | :------------------------------- | | `overtime_request_create` | An overtime request is created. | | `overtime_request_update` | An overtime request is updated. | | `overtime_request_destroy` | An overtime request is deleted. | | `overtime_request_approve` | An overtime request is approved. | ### Other [#other] | Topic | Description | | :------------- | :------------------------------------------------------ | | `survey_start` | A survey is launched (moves from Scheduled to Running). | A **workflow** webhook can also be configured as an *action* inside a workflow — it isn't part of the subscribable topic list above. See [Other webhooks](./other-webhooks.mdx). ## Delivery headers [#delivery-headers] Every delivery includes: * `Content-Type: application/json` * `X-PeopleForce-Endpoint` — the webhook endpoint ID * `X-PeopleForce-Delivery` — a unique delivery ID * `X-PeopleForce-Signature` — present only when the endpoint has a secret configured (see [signing](../getting-started/starting-with-webhooks.mdx#secrets)) Deliveries time out after 5 seconds and require a valid TLS certificate. The pages in this section group these topics and show example payloads: [Employee profile](./employee-profile.mdx), [Employee position](./employee-position.mdx), [Employee job profile](./employee-job-profile.mdx), [Employee custom table rows](./employee-table-rows.mdx), [Employee compensation](./employee-compensation.mdx), [Leave requests](./leave-requests.mdx), [Candidates and vacancies](./candidates-and-vacancies.mdx), [Overtime requests](./overtime-requests.mdx), and [Other webhooks](./other-webhooks.mdx). # Leave requests (/company/v1/webhooks/leave-requests) PeopleForce offers five webhook topics covering the leave-request lifecycle: * `leave_request_create` — a leave request is created * `leave_request_approve` — a leave request is approved * `leave_request_reject` — a leave request is rejected * `leave_request_withdraw` — a leave request is withdrawn * `leave_request_destroy` — a leave request is deleted If the leave policy has file attachments enabled or required, the attached files are not included in the webhook payload. ## Leave request created [#leave-request-created] Triggered instantly, as soon as the leave request is created. Action: `leave_request_create`. ```json { "action": "leave_request_create", "data": { "id": 1062480, "attributes": { "employee_id": 2632, "starts_on": "2024-01-31", "ends_on": "2024-02-01", "amount": "2.0", "description": "Vacation with family", "unit": "days", "state": "approved" }, "entries": [ { "occurs_on": "2024-01-31", "amount": "1.0" }, { "occurs_on": "2024-02-01", "amount": "1.0" } ], "leave_type": { "id": 13289, "name": "Vacation" }, "meta": { "created_at": "2024-02-01T15:43:50.841+00:00", "updated_at": "2024-02-01T15:43:50.878+00:00" } } } ``` ## Leave request approved [#leave-request-approved] Triggered instantly, as soon as the last approver in the approval flow approves the request and its status changes to Approved. Action: `leave_request_approve`. ```json { "action": "leave_request_approve", "data": { "id": 345842, "attributes": { "employee_id": 2632, "starts_on": "2022-09-20", "ends_on": "2022-09-23", "amount": "32.0", "description": "", "unit": "hours", "state": "approved" }, "entries": [ { "occurs_on": "2022-09-20", "amount": "8.0" }, { "occurs_on": "2022-09-21", "amount": "8.0" }, { "occurs_on": "2022-09-22", "amount": "8.0" }, { "occurs_on": "2022-09-23", "amount": "8.0" } ], "leave_type": { "id": 4098, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:08:31.562+01:00", "updated_at": "2022-09-21T16:08:36.540+01:00" } } } ``` ## Leave request rejected [#leave-request-rejected] Triggered instantly, as soon as at least one approver in the approval flow rejects the request and its status changes to Rejected. Action: `leave_request_reject`. ```json { "action": "leave_request_reject", "data": { "id": 345847, "attributes": { "employee_id": 2632, "starts_on": "2022-09-06", "ends_on": "2022-09-11", "amount": "32.0", "description": "", "unit": "hours", "state": "rejected" }, "entries": [ { "occurs_on": "2022-09-06", "amount": "8.0" }, { "occurs_on": "2022-09-07", "amount": "8.0" }, { "occurs_on": "2022-09-08", "amount": "8.0" }, { "occurs_on": "2022-09-09", "amount": "8.0" }, { "occurs_on": "2022-09-10", "amount": "0.0" }, { "occurs_on": "2022-09-11", "amount": "0.0" } ], "leave_type": { "id": 4098, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:10:25.052+01:00", "updated_at": "2022-09-21T16:10:28.284+01:00" } } } ``` ## Leave request withdrawn [#leave-request-withdrawn] Triggered instantly, as soon as the leave request is withdrawn and its status changes to Withdrawn. Action: `leave_request_withdraw`. ```json { "action": "leave_request_withdraw", "data": { "id": 345850, "attributes": { "employee_id": 2632, "starts_on": "2022-09-28", "ends_on": "2022-09-30", "amount": "3.0", "description": "", "unit": "days", "state": "withdrawn" }, "entries": [ { "occurs_on": "2022-09-28", "amount": "1.0" }, { "occurs_on": "2022-09-29", "amount": "1.0" }, { "occurs_on": "2022-09-30", "amount": "1.0" } ], "leave_type": { "id": 3916, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:12:48.034+01:00", "updated_at": "2022-09-21T16:13:13.260+01:00" } } } ``` # Other webhooks (/company/v1/webhooks/other-webhooks) ## Workflow triggered [#workflow-triggered] This webhook can only be configured as an action inside a workflow. The payload carries the full employee record the workflow acts on. ```json { "data": { "id": 6369, "attributes": { "employee_number": null, "hired_on": "2017-08-21", "first_name": "Scott", "middle_name": "", "last_name": "Pilgrim", "email": "scott@example.com", "termination_effective_date": null, "termination_reason": null, "termination_type": null }, "reporting_to": null, "position": null, "department": { "id": 105328, "name": "Corporate" }, "division": null, "location": { "id": 3134, "name": "Spain" }, "custom_fields": { "a25b06fb-bcfb-4aac-b695-6aff138fac36": { "name": "HR manager", "value": null, "group": "Personal" } }, "meta": { "created_at": "2020-02-03T12:08:03.934+01:00", "updated_at": "2025-10-20T15:42:42.928+02:00" } } } ``` ## Survey launched [#survey-launched] Triggered instantly when a survey moves from "Scheduled" to "Running". Action: `survey_start`. ```json { "action": "survey_start", "data": { "id": 10441, "attributes": { "name": "Stress management", "state": "running", "locale": "en", "starts_at": "2024-02-01T00:00:00.000Z", "ends_at": "2024-02-29T00:00:00.000Z" }, "meta": { "created_at": "2024-02-01T16:32:17.956Z", "updated_at": "2024-02-01T16:34:00.563Z" } } } ``` # Overtime requests (/company/v1/webhooks/overtime-requests) PeopleForce offers four webhook topics covering the overtime-request lifecycle: * `overtime_request_create` — an overtime request is created * `overtime_request_update` — an overtime request is updated * `overtime_request_approve` — an overtime request is approved * `overtime_request_destroy` — an overtime request is deleted ## Overtime request created [#overtime-request-created] Triggered instantly, as soon as the overtime request is created. Action: `overtime_request_create`. ```json { "action": "overtime_request_create", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T20:30:00.000+00:00", "minutes": 150, "comment": "Release deployment support", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:12:03.841+00:00" } } } ``` ## Overtime request updated [#overtime-request-updated] Triggered instantly, as soon as the overtime request is updated. Action: `overtime_request_update`. ```json { "action": "overtime_request_update", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:20:47.116+00:00" } } } ``` ## Overtime request approved [#overtime-request-approved] Triggered instantly, as soon as the last approver in the approval flow approves the request and its status changes to Approved. Action: `overtime_request_approve`. ```json { "action": "overtime_request_approve", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "approved" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T10:05:12.298+00:00" } } } ``` ## Overtime request deleted [#overtime-request-deleted] Triggered instantly, as soon as the overtime request is deleted. Action: `overtime_request_destroy`. ```json { "action": "overtime_request_destroy", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:12:03.841+00:00" } } } ``` # Authentication (/company/v2/getting-started/authentication) PeopleForce authenticates API requests with an API key. There are **two types of key** depending on your use case — choose the one that matches your goal. | Key type | Access | Where it can be used | | :------------------ | :----------------------------------------------------------------------------- | :------------------------------------------------------------------------ | | **Company API key** | Full access to the PeopleForce API and people data, with minimal restrictions. | Server-to-server integrations only. **Never** on a public-facing website. | | **Career API key** | Limited to non-sensitive, vacancy-specific data. | Safe to use on a public-facing website (e.g. a custom careers page). | To create a key, go to **Settings → API keys** (at the bottom of the page) → **Generate API key**. ## Company API key [#company-api-key] A Company API key lets the holder retrieve or change data in your company account through the API. Use it to build any system integration to PeopleForce. A Company API key grants access to nearly all data in PeopleForce. Only use it in trusted server-to-server integrations, never on a public-facing website, and only share it with developers you trust. You can scope a Company key with these restrictions: * **People compensation** — limit viewing or editing of people compensation. * **Vacancy salary range** — limit viewing or editing of vacancy salary ranges. * **Candidate desired salary** — limit viewing or editing of candidate desired salary. * **Candidate sensitive fields** — limit viewing or editing of candidate sensitive fields. * **IP addresses** — restrict callers to an allow-list of IP addresses (e.g. office or home networks). Generating a Company API key ## Career API key [#career-api-key] A Career API key lets you retrieve vacancies from PeopleForce to build a custom careers page on your own website. Because it is limited to non-sensitive, vacancy-specific information, it is safe to embed in a public-facing site. See [Own career site integration](/careers/v1/guides/own-career-site) for how to use it. Generating a Career API key ## Using a key [#using-a-key] Pass the key in a request header named **`X-API-KEY`**: ```bash curl https://app.peopleforce.io/api/public/v2/employees \ -H "X-API-KEY: " ``` All API requests must be made over HTTPS — calls over plain HTTP will fail, and so will requests without authentication. Passing the API key in the X-API-KEY header ## Disabling a key [#disabling-a-key] From the API key list page you can **disable** a key to temporarily stop it from working without deleting it — useful while pausing an integration. The API keys list ## Revoking a key [#revoking-a-key] If you no longer need a key, delete it from **Settings → API keys** → find the key → **Delete**. Deleting an API key from the list Deleting an API key is permanent and immediate. Any integration using that key stops working at once, and the key cannot be recreated. Account for every integration before deleting. ## Troubleshooting [#troubleshooting] ### 401 Unauthorized [#401-unauthorized] Double-check that the API key was copied correctly and try again. ```json { "message": "Bad Credentials" } ``` ### 403 Forbidden [#403-forbidden] Your role doesn't have permission for this action — most often because the key is restricted from compensation data. ### 404 Not Found [#404-not-found] The resource could not be found. Check that your request refers to an existing object. ### 422 Unprocessable Entity [#422-unprocessable-entity] One or more fields failed validation. The response body lists every error: ```json { "success": false, "errors": [ "Field name can't be blank" ] } ``` ### 500 Internal Server Error [#500-internal-server-error] A problem on our side. Try again later, or [contact support](mailto:support@peopleforce.io). # FAQ (/company/v2/getting-started/faq) ## Is using the API free? [#is-using-the-api-free] Yes — the API is free for all PeopleForce clients, and we encourage everyone to use it. ## What are the API rate limits? [#what-are-the-api-rate-limits] The current limit is **300 requests per minute**. See [Rate limits](../api-basics/rate-limit.mdx) for details. ## I don't see Webhooks, API, or Settings in PeopleForce [#i-dont-see-webhooks-api-or-settings-in-peopleforce] These pages are only visible to admin users, or users with the webhook or API permission. Contact your PeopleForce administrator to request access. ## I found a bug / I got a 500 error [#i-found-a-bug--i-got-a-500-error] Sorry to hear it — we aim for the best quality in our product. If you hit an issue, [let us know](mailto:support@peopleforce.io) and we'll work on it. A `500` response automatically notifies our team, but you're still welcome to write in and describe what happened — it helps us resolve it faster, and we'll let you know once there's a fix. ## I couldn't find the endpoint or data I was looking for [#i-couldnt-find-the-endpoint-or-data-i-was-looking-for] We cover a wide range of cases across the API and webhooks, but maybe yours is new to us. [Let us know](mailto:support@peopleforce.io) and we'll do our best to help — see also [Request a feature](./request-a-feature.mdx). # Introduction (/company/v2/getting-started) You can use the PeopleForce Company API to access your PeopleForce data — retrieving information about the entities stored in your account and performing actions on them. The API is organised around REST: predictable resource-oriented URLs, JSON request and response bodies, and standard HTTP verbs and status codes. For example, you can sync employees into PeopleForce from an external system, keep your lists of departments, divisions, and positions up to date, read time-off balances, or pull engagement-survey results. You'll need an API key first; see [Authentication](./authentication.mdx) to generate one. Then browse the full endpoint list under **API reference** in the sidebar — every operation has a live "Send" panel. ## Explore the platform's capabilities [#explore-the-platforms-capabilities] * **[Authentication](./authentication.mdx)** — generate an API key and authenticate every request. * **[Webhooks](../webhooks/index.mdx)** — subscribe to events and get notified the moment data changes in PeopleForce. Use the version switcher at the top of the sidebar to move between API versions. This page applies to every version. If you have any issues or questions, [contact us](mailto:support@peopleforce.io). # Request a feature (/company/v2/getting-started/request-a-feature) Didn't find what you were looking for in the API or webhooks? Let us know. We keep the API broad and already cover the common use cases — but if something is missing, send us a feature request and we'll see whether it can be added. [Open the requests board →](https://feedback.peopleforce.io/b/for-devs) # Starting with webhooks (/company/v2/getting-started/starting-with-webhooks) Webhooks let external services be notified when certain events happen in PeopleForce. When a subscribed event occurs, PeopleForce sends a `POST` request to each URL you configure. ## Setting up a webhook [#setting-up-a-webhook] Go to **Settings → Webhooks → Add new webhook** and fill in four fields: * **Name** — a label so you can find the webhook later. * **Payload URL** — the server that receives the webhook `POST` requests. It must use a valid SSL certificate from a publicly trusted CA. * **Secret** — an optional string used to sign requests (see below). * **Topics** — the events you want to be notified about; you can choose multiple. Save the webhook and it's live. See the full list of events in the [Webhooks overview](../webhooks/index.mdx). Creating a new webhook in Settings ## Secrets [#secrets] A secret is a shared string used to authenticate webhook deliveries. If you set one, PeopleForce signs each request and adds an `x-peopleforce-signature` header. Without the secret, no one else can forge a request with a matching signature. To verify the signature: * It is computed with **HMAC-SHA256** over the raw request body, keyed with your secret. * The result is a hexadecimal digest, prefixed with `sha256=`. * Compare it against the `x-peopleforce-signature` header using a **constant-time** comparison to avoid timing attacks. * Treat the payload as UTF-8 encoded text. ```ruby title="Ruby" def verify_signature(payload_body, signature_header) secret_key = ENV['WEBHOOK_SECRET'] computed_signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret_key, payload_body) unless Rack::Utils.secure_compare(computed_signature, signature_header) return halt 500, "Signatures didn't match!" end end ``` ```python title="Python" import hmac import hashlib import os def verify_signature(payload_body, signature_header): secret_token = os.getenv('WEBHOOK_SECRET') if not signature_header: raise HTTPException(status_code=403, detail="Missing signature header!") hash_object = hmac.new(key=secret_token.encode('utf-8'), msg=payload_body.encode('utf-8'), digestmod=hashlib.sha256) expected_signature = 'sha256=' + hash_object.hexdigest() if not hmac.compare_digest(expected_signature, signature_header): raise HTTPException(status_code=403, detail="Signatures did not match!") ``` ## Testing a webhook [#testing-a-webhook] When an event fires, PeopleForce delivers the JSON payload as the body of the `POST` request. To try it out, point the Payload URL at a request inspector such as [webhook.site](https://webhook.site/) (external link — be mindful of your data), then trigger the event. For a new employee, the payload looks like: ```json { "action": "employee_create", "data": { "id": 130333, "attributes": { "employee_number": "PF124593", "hired_on": "2022-09-12", "probation_ends_on": "2022-12-12", "first_name": "John", "last_name": "Doe", "email": "john@peopleforce.io", "personal_email": null, "gender": "male", "mobile_number": "", "work_phone_number": "", "date_of_birth": "1978-08-31", "termination_effective_date": null, "termination_comment": null, "avatar_url": null }, "reporting_to": { "id": 5844, "full_name": "Ross Kate", "email": "kate@peopleforce.io" }, "employment_type": { "id": 1899, "name": "Full-Time" }, "position": { "id": 11943, "name": "Senior Developer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": null, "custom_fields": { "2692d87e-c388-49ea-b903-616bc1557746": { "name": "T-shirt size", "value": "M", "group": "Personal" } }, "meta": { "created_at": "2022-09-05T18:33:55.130+03:00", "updated_at": "2022-09-05T18:33:55.526+03:00" } } } ``` Webhook payloads are emitted by the PeopleForce platform and use the platform's own field names — they are independent of the REST API version you call. ## Execution history [#execution-history] To confirm a delivery, go to **Settings → Webhooks** and click a webhook's name to see its execution history. Click any delivery to view the raw data that was sent. Webhook execution history ## Troubleshooting [#troubleshooting] If a delivery didn't arrive or didn't look right: 1. Check the Payload URL is correct and the receiving server supports webhooks. 2. Check the webhook's run history. If it was delivered, re-check step 1, then step 3. 3. Make sure your receiving server is up and healthy. | Status | Meaning | | :---------------------------- | :------------------------------------------------------------------------------------------- | | **200 Success** | Webhook was delivered. | | **404 Not Found** | The resource could not be found — check it refers to an existing object. | | **500 Internal Server Error** | A problem on our side — try again later or [contact support](mailto:support@peopleforce.io). | If none of these help, [contact us](mailto:support@peopleforce.io). # Candidates and vacancies (/company/v2/webhooks/candidates-and-vacancies) For PeopleRecruit users, these webhooks cover the recruitment lifecycle: * `applicant_create` — a candidate is created * `vacancy_offer_accept` — a candidate accepted and signed an offer * `vacancy_create` — a vacancy is created * `vacancy_application_create` — a candidate applied or was added to a vacancy ## Applicant created [#applicant-created] Triggered instantly when a new applicant is created. Action: `applicant_create`. Custom fields are keyed by their field ID, each with a `name` and `value`. ```json { "action": "applicant_create", "data": { "id": 1461719, "attributes": { "full_name": "Pavlo Skrypka", "position": null, "email": "pavlo@example.com", "phone_numbers": ["380948593490"], "urls": [] }, "custom_fields": { "70f2c0d7-559a-4066-be3a-1cc53e547c52": { "name": "Driving license", "value": null }, "c162e147-7c10-4bee-b53b-72f0183edbc2": { "name": "Special notes", "value": null }, "18700c50-a0ac-40e1-8e52-f636a64a3edc": { "name": "desired_salary", "value": null } }, "meta": { "created_at": "2022-09-21T16:17:10.410+01:00", "updated_at": "2022-09-21T16:17:10.416+01:00" } } } ``` ## Applicant offer accepted [#applicant-offer-accepted] Triggered instantly when an applicant accepts and signs an offer. Action: `vacancy_offer_accept`. The payload does not include the offer document. ```json { "action": "vacancy_offer_accept", "data": { "id": 7978, "attributes": { "accepted_at": "2022-09-21T16:20:21.035+01:00", "rejected_at": null, "viewed_at": "2022-09-21T16:20:17.229+01:00" }, "applicant": { "id": 1461719, "full_name": "Pavlo Skrypka", "email": "pavlo@example.io" }, "vacancy": { "id": 526, "title": "QA Tester", "status": "accepted" }, "meta": { "created_at": "2022-09-21T16:19:58.757+01:00", "updated_at": "2022-09-21T16:20:21.013+01:00" } } } ``` ## Vacancy created [#vacancy-created] Triggered instantly when a vacancy is created. Action: `vacancy_create`. The `description` is delivered as HTML. ```json { "action": "vacancy_create", "data": { "id": 56721, "attributes": { "name": "UX Designer", "description": "
" }, "custom_fields": { "478214e1-0b81-4048-a6a3-63d3088d1ee0": { "name": "Custom field", "value": "Custom text" } }, "meta": { "created_at": "2024-02-01T16:20:40.896+00:00", "updated_at": "2024-02-01T16:20:40.906+00:00" } } } ``` ## Vacancy application created [#vacancy-application-created] Triggered instantly when a candidate applies or is added to a vacancy. Action: `vacancy_application_create`. ```json { "action": "vacancy_application_create", "data": { "id": 3102839, "candidate": { "id": 2151205, "full_name": "Jasmin Doe", "email": null }, "vacancy": { "id": 56721, "title": "UX Designer" }, "meta": { "created_at": "2024-02-01T16:27:14.814+00:00", "updated_at": "2024-02-01T16:27:14.814+00:00" } } } ``` # Employee compensation (/company/v2/webhooks/employee-compensation) Alongside an employee's profile attributes, PeopleForce keeps separate records for **position** and **salary**. To preserve the history of salary reviews and other changes, add a new record rather than editing the existing one. The salary record emits these webhooks: * `employee_salary_create` — a salary record is added * `employee_salary_update` — a salary record is updated Both fire immediately for any employee, active or terminated. ## Employee salary created [#employee-salary-created] Action: `employee_salary_create`. ```json { "action": "employee_salary_create", "data": { "id": 80093, "attributes": { "effective_on": "2022-09-21", "amount": "45000.0", "per": "month", "currency_code": "UAH", "comment": "" }, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com" }, "meta": { "created_at": "2022-09-21T15:59:57.173+01:00", "updated_at": "2022-09-21T15:59:57.173+01:00" } } } ``` ## Employee salary updated [#employee-salary-updated] Action: `employee_salary_update`. The payload shape matches `employee_salary_create`, with the updated values. # Employee job profile (/company/v2/webhooks/employee-job-profile) Alongside an employee's profile attributes, PeopleForce keeps a **job profile** history for each employee. As with position and salary, add a new record rather than editing the existing one so the history of moves is preserved. The job profile record emits these webhooks: * `employee_job_profile_create` — a job profile record is added * `employee_job_profile_update` — a job profile record is updated Both fire immediately for any employee, active or terminated. ## When they fire [#when-they-fire] A delivery is sent when the record is saved through any of these paths: * the **Job profile** section of an employee's profile * the Public API (`POST` / `PATCH` on an employee's job profiles) * an approved employee change request that adds a job profile record — this emits `employee_job_profile_create` * compliance workforce-planning verification, which adds a record `employee_job_profile_update` fires only when the save changes at least one stored value. A request that submits identical values sends nothing. A failed save sends nothing. ## Employee job profile created [#employee-job-profile-created] Action: `employee_job_profile_create`. ```json { "action": "employee_job_profile_create", "data": { "id": 4, "attributes": { "effective_on": "2026-08-14" }, "employee": { "id": 6, "employee_number": "IT-1029", "first_name": "Charles", "last_name": "Lancaster", "email": "charles@example.com" }, "job_profile": { "id": 2, "name": "Senior Developer" }, "job_group": { "id": 1, "name": "Engineering" }, "meta": { "created_at": "2026-08-14T14:18:40.723Z", "updated_at": "2026-08-14T14:18:40.723Z" } } } ``` ## Employee job profile updated [#employee-job-profile-updated] Action: `employee_job_profile_update`. The payload shape matches `employee_job_profile_create`, with the updated values. ```json { "action": "employee_job_profile_update", "data": { "id": 2, "attributes": { "effective_on": "2026-08-01" }, "employee": { "id": 4, "employee_number": null, "first_name": "Alex", "last_name": "Johnstone", "email": "alex@example.com" }, "job_profile": { "id": 1, "name": "Developer" }, "job_group": { "id": 1, "name": "Engineering" }, "meta": { "created_at": "2026-08-14T14:15:40.159Z", "updated_at": "2026-08-14T14:18:09.037Z" } } } ``` ## Field reference [#field-reference] | Field | Type | Notes | | :----------------------------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------- | | `data.id` | integer | The job profile history record ID, not the job profile ID. | | `data.attributes.effective_on` | date | The date the record takes effect. | | `data.employee` | object | Employee identity: `id`, `employee_number`, `first_name`, `last_name`, and `email`. `employee_number` is `null` when unset. | | `data.job_profile` | object | The assigned job profile: `id` and `name`. | | `data.job_group` | object or null | The job group the profile belongs to: `id` and `name`. `null` if the profile has no job group. | | `data.meta` | object | `created_at` and `updated_at` for the history record. | # Employee position (/company/v2/webhooks/employee-position) Alongside an employee's profile attributes (name, hire date, and so on), PeopleForce keeps separate records for **position** and **salary**. To preserve the history of promotions and other changes, add a new record rather than editing the existing one. The position record emits these webhooks: * `employee_position_create` — a position record is added * `employee_position_update` — a position record is updated Both fire immediately for any employee, active or terminated. ## Employee position created [#employee-position-created] Action: `employee_position_create`. ```json { "action": "employee_position_create", "data": { "id": 224222, "attributes": { "effective_on": "2022-12-01" }, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com" }, "reporting_to": { "id": 10277, "first_name": "Pahney", "last_name": "Zhelezo" }, "position": { "id": 11943, "name": "Senior Developer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": { "id": 28693, "name": "Vinnytsia" }, "meta": { "created_at": "2022-09-21T14:57:29.151+01:00", "updated_at": "2022-09-21T14:57:29.151+01:00" } } } ``` ## Employee position updated [#employee-position-updated] Action: `employee_position_update`. The payload shape matches `employee_position_create`, with the updated values. # Employee profile (/company/v2/webhooks/employee-profile) An employee profile is the account of an employee. Employees may log in using their email or Active Directory username, so take care when changing those fields. The employee profile emits these webhooks: * `employee_create` — an employee profile is created * `employee_update` — an employee profile is updated * `employee_start` — an employee's first day * `employee_terminate` — an employee is terminated * `employee_termination_revert` — a termination is reverted (reactivation) Webhook payloads are emitted by the PeopleForce platform and use the platform's own field names (e.g. `position`, `employee_number`) — independent of the Company API v4 resource naming. ## Employee profile created [#employee-profile-created] Triggered instantly when a new employee profile is created. Action: `employee_create`. ```json { "action": "employee_create", "data": { "id": 133710, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-09-26", "probation_ends_on": "2022-12-26", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com", "personal_email": null, "gender": "male", "mobile_number": "", "work_phone_number": "", "date_of_birth": "1978-08-29", "termination_effective_date": null, "termination_comment": null, "avatar_url": null }, "reporting_to": { "id": 10277, "full_name": "Zhelezo Pahney", "email": "zp@peopleforce.io" }, "employment_type": { "id": 1899, "name": "Full-Time" }, "position": { "id": 114651, "name": "IT Support Engineer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": { "id": 28693, "name": "Vinnytsia" }, "custom_fields": { "a25b06fb-bcfb-4aac-b695-6aff138fac36": { "name": "HR manager", "value": "5844", "group": "Personal" } }, "meta": { "created_at": "2022-09-21T16:29:07.894+03:00", "updated_at": "2022-09-21T16:29:08.713+03:00" } } } ``` ## Employee profile updated [#employee-profile-updated] Triggered instantly when an existing employee profile is updated. Action: `employee_update`. The payload shape matches `employee_create`, with the updated field values. ## Employee first day [#employee-first-day] Triggered in two cases: 1. Instantly, if an employee is created with a hire date of today. 2. Every night at 01:00 (UTC) for all employees whose hire date is that day. Action: `employee_start`. The payload shape matches `employee_create`. ```json { "action": "employee_start", "data": { "id": 133710, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-09-12", "first_name": "John", "last_name": "Doe", "email": "john@peopleforce.io" }, "meta": { "created_at": "2022-09-05T15:33:55.130Z", "updated_at": "2022-09-05T15:33:55.526Z" } } } ``` ## Employee terminated [#employee-terminated] Triggered in two cases: 1. Instantly, if an employee is terminated with a past date. 2. Every night at 01:00 (UTC) for all employees whose termination date is that day. Action: `employee_terminate`. The payload shape matches `employee_create`, with `termination_effective_date` set and most relational fields `null`. ```json { "action": "employee_terminate", "data": { "id": 123512, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-07-26", "first_name": "John", "last_name": "Doe", "termination_effective_date": "2022-11-07", "termination_comment": "" }, "reporting_to": null, "position": null, "department": null, "division": null, "location": null, "meta": { "created_at": "2022-07-29T10:42:43.354+01:00", "updated_at": "2022-11-10T11:12:32.102+00:00" } } } ``` ## Termination reverted [#termination-reverted] Triggered when an employee's termination is reverted — either cancelled before the termination date, or a terminated employee is reactivated. Action: `employee_termination_revert`. The payload shape matches `employee_create`. There is no `employee_activate` webhook. Reactivation and un-termination are delivered as `employee_termination_revert`. # Employee custom table rows (/company/v2/webhooks/employee-table-rows) **Custom tables** are the repeating tables you define yourself on an employee's profile — certifications, equipment, dependants, and so on. Each row emits its own event, so an integration can stay in step without polling. The custom table row emits these webhooks: * `employee_table_row_create` — a row is added * `employee_table_row_update` — a row is changed * `employee_table_row_destroy` — a row is deleted Subscribe to each one independently. Selecting one does not subscribe you to the other two, and existing endpoints gain none of them until you add them. Every payload is self-contained: it carries the employee, the table, and every column with its current value, so you do not need a follow-up API call. Values are the current state only — there is no before-and-after comparison. ## When they fire [#when-they-fire] A delivery is sent when the row is saved or deleted through any of these paths: * the employee's profile * the Public API (`POST` / `PATCH` / `DELETE` on an employee's table rows) * an approved employee change request that adds a row — this emits `employee_table_row_create` One event is sent per changed row, so a change request touching three rows sends three deliveries. ### When they do not fire [#when-they-do-not-fire] These topics cover **custom** tables only. Rows in PeopleForce system tables — position, salary, employment history, and compliance tables — never emit them. Nothing is sent when: * the save changes no stored value, so an identical update is silent * the create, update, delete, or approval fails * a change request is submitted, rejected, or cancelled — only approval emits * a row is created by a **hire form or preboarding form** ## Employee custom table row created [#employee-custom-table-row-created] Action: `employee_table_row_create`. ```json { "action": "employee_table_row_create", "data": { "id": 88213, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" }, "table": { "id": 4471, "name": "Certifications", "internal_name": "certifications" }, "columns": [ { "id": 90114, "name": "Level", "internal_name": "level", "type": "EmployeeTableColumns::Text", "value": "Professional" }, { "id": 90115, "name": "Certified on", "internal_name": "certified_on", "type": "EmployeeTableColumns::Date", "value": null }, { "id": 90116, "name": "Score", "internal_name": "score", "type": "EmployeeTableColumns::Number", "value": "87" }, { "id": 90117, "name": "Renewable", "internal_name": "renewable", "type": "EmployeeTableColumns::CheckBox", "value": "1" }, { "id": 90118, "name": "Rating", "internal_name": "rating", "type": "EmployeeTableColumns::SingleSelect", "value": { "id": "3312", "name": "Excellent" } }, { "id": 90119, "name": "Topics", "internal_name": "topics", "type": "EmployeeTableColumns::MultipleSelect", "value": [ { "id": "3315", "name": "Ruby" }, { "id": "3316", "name": "Rails" } ] }, { "id": 90120, "name": "Mentor", "internal_name": "mentor", "type": "EmployeeTableColumns::Reference", "value": { "id": "133711", "name": "Grace Hopper" } }, { "id": 90121, "name": "Notes", "internal_name": "notes", "type": "EmployeeTableColumns::LongText", "value": "Renewal due next year" } ], "meta": { "created_at": "2026-08-17T09:41:12.204Z", "updated_at": "2026-08-17T09:41:12.204Z" } } } ``` ## Employee custom table row updated [#employee-custom-table-row-updated] Action: `employee_table_row_update`. The payload shape matches `employee_table_row_create`, with the row's values after the change. ## Employee custom table row deleted [#employee-custom-table-row-deleted] Action: `employee_table_row_destroy`. The payload shape matches `employee_table_row_create`, and carries the row's final values as they stood before deletion. ## Field reference [#field-reference] | Field | Type | Notes | | :----------------------------- | :------ | :------------------------------------------------------------------------------------------ | | `data.id` | integer | The row ID. | | `data.employee` | object | Employee identity: `id`, `employee_number`, `first_name`, `last_name`, and `email`. | | `data.table` | object | The custom table: `id`, `name`, and `internal_name`. | | `data.columns` | array | Every column on the table, in the order shown in the product. Deleted columns are excluded. | | `data.columns[].id` | integer | The column ID. | | `data.columns[].internal_name` | string | The stable API name. Prefer it over `name`, which admins can rename freely. | | `data.columns[].type` | string | The column type — see the table below. | | `data.columns[].value` | varies | The current value — see the table below. | | `data.meta` | object | `created_at` and `updated_at` for the row. | ### Column values by type [#column-values-by-type] Every column on the table appears in `columns`, whether or not it holds a value. An empty column is present with a `null` value. | `type` | `value` shape | Empty | | :------------------------------------- | :------------------------------------------ | :----- | | `EmployeeTableColumns::Text` | string | `null` | | `EmployeeTableColumns::Number` | string, e.g. `"87"` | `null` | | `EmployeeTableColumns::Date` | string, e.g. `"2026-08-14"` | `null` | | `EmployeeTableColumns::CheckBox` | string, `"1"` when ticked or `"0"` when not | `null` | | `EmployeeTableColumns::LongText` | string, HTML stripped to plain text | `null` | | `EmployeeTableColumns::SingleSelect` | `{ "id": "…", "name": "…" }` | `null` | | `EmployeeTableColumns::MultipleSelect` | array of `{ "id": "…", "name": "…" }` | `[]` | | `EmployeeTableColumns::Reference` | `{ "id": "…", "name": "…" }` — an employee | `null` | Four details to code against: * **Scalars are JSON strings, not JSON numbers or booleans.** A number column sends `"87"`, a date sends `"2026-08-14"`, and a checkbox sends `"1"` or `"0"`. Both write paths store the value as text and the payload returns it uncast, so parse rather than assume. Rows loaded by an older import may still hold a raw JSON number. * An **unticked** checkbox sends `"0"`, which is a real value, not an empty one. Only a checkbox that was never set sends `null`. * A multi-select is **always an array**, never `null`. An empty one is `[]`. * The `id` inside a select, multi-select, or reference value is a **string**, because that is how the row stores it. The surrounding `columns[].id` and `data.id` are integers. A reference value keeps its stored `id` even when the referenced employee can no longer be found, in which case `name` is `null`. ## Testing a payload [#testing-a-payload] The sample-payload endpoint (`GET /webhooks?key=`) does not yet cover the three custom table row topics and returns `204 No Content` for them. To see a real payload, subscribe an endpoint, change a row, then open the delivery in **Settings → Webhooks → delivery history**. # Webhooks overview (/company/v2/webhooks) Webhooks let you subscribe to events happening in PeopleForce. Rather than polling the API, you configure an endpoint and PeopleForce sends it an HTTP `POST` request whenever a subscribed event fires. You manage subscriptions — and review past deliveries and their payloads — in your PeopleForce **Settings**. See [Starting with webhooks](../getting-started/starting-with-webhooks.mdx) for how to create one, secure it with a signing secret, and test deliveries. Every delivery has the shape `{ "action": "", "data": { … } }`, where `action` is the topic name (e.g. `employee_create`). Doesn't your integration support receiving webhooks? Check whether it supports Zapier — PeopleForce offers a range of [triggers and actions for Zapier](https://zapier.com/apps/peopleforce/integrations). ## Available topics [#available-topics] PeopleForce offers the webhook topics below. Availability depends on the PeopleForce modules enabled for your account. ### Employee [#employee] | Topic | Description | | :---------------------------------------- | :---------------------------------------------------- | | `employee_create` | An employee is created. | | `employee_update` | An employee is updated. | | `employee_start` | An employee's first day (hire date is reached). | | `employee_terminate` | An employee is terminated. | | `employee_termination_revert` | An employee's termination is reverted (reactivation). | | `employee_position_create` | An employee position record is created. | | `employee_position_update` | An employee position record is updated. | | `employee_job_profile_create` | An employee job profile record is created. | | `employee_job_profile_update` | An employee job profile record is updated. | | `employee_table_row_create` | A row in an employee's custom table is created. | | `employee_table_row_update` | A row in an employee's custom table is updated. | | `employee_table_row_destroy` | A row in an employee's custom table is deleted. | | `employee_salary_create` | An employee salary record is created. | | `employee_salary_update` | An employee salary record is updated. | | `employee_additional_compensation_create` | An additional compensation is created. | | `employee_additional_compensation_update` | An additional compensation is updated. | | `employee_employment_status_create` | An employment status record is created. | | `employee_employment_status_update` | An employment status record is updated. | | `external_user_create` | An external user is created. | ### Leave [#leave] | Topic | Description | | :----------------------- | :---------------------------- | | `leave_request_create` | A leave request is created. | | `leave_request_approve` | A leave request is approved. | | `leave_request_reject` | A leave request is rejected. | | `leave_request_withdraw` | A leave request is withdrawn. | | `leave_request_destroy` | A leave request is deleted. | ### Recruitment [#recruitment] | Topic | Description | | :----------------------------- | :--------------------------------------------------------------------- | | `applicant_create` | A candidate is created. | | `applicant_destroy` | A candidate is deleted. | | `vacancy_create` | A vacancy is created. | | `vacancy_application_create` | A candidate applied or was added to a vacancy. | | `vacancy_application_movement` | An application moved pipeline stage, was disqualified, or requalified. | | `vacancy_offer_accept` | A vacancy offer was accepted by the candidate. | | `vacancy_offer_reject` | A vacancy offer was rejected by the candidate. | ### Time [#time] | Topic | Description | | :------------------------- | :------------------------------- | | `overtime_request_create` | An overtime request is created. | | `overtime_request_update` | An overtime request is updated. | | `overtime_request_destroy` | An overtime request is deleted. | | `overtime_request_approve` | An overtime request is approved. | ### Other [#other] | Topic | Description | | :------------- | :------------------------------------------------------ | | `survey_start` | A survey is launched (moves from Scheduled to Running). | A **workflow** webhook can also be configured as an *action* inside a workflow — it isn't part of the subscribable topic list above. See [Other webhooks](./other-webhooks.mdx). ## Delivery headers [#delivery-headers] Every delivery includes: * `Content-Type: application/json` * `X-PeopleForce-Endpoint` — the webhook endpoint ID * `X-PeopleForce-Delivery` — a unique delivery ID * `X-PeopleForce-Signature` — present only when the endpoint has a secret configured (see [signing](../getting-started/starting-with-webhooks.mdx#secrets)) Deliveries time out after 5 seconds and require a valid TLS certificate. The pages in this section group these topics and show example payloads: [Employee profile](./employee-profile.mdx), [Employee position](./employee-position.mdx), [Employee job profile](./employee-job-profile.mdx), [Employee custom table rows](./employee-table-rows.mdx), [Employee compensation](./employee-compensation.mdx), [Leave requests](./leave-requests.mdx), [Candidates and vacancies](./candidates-and-vacancies.mdx), [Overtime requests](./overtime-requests.mdx), and [Other webhooks](./other-webhooks.mdx). # Leave requests (/company/v2/webhooks/leave-requests) PeopleForce offers five webhook topics covering the leave-request lifecycle: * `leave_request_create` — a leave request is created * `leave_request_approve` — a leave request is approved * `leave_request_reject` — a leave request is rejected * `leave_request_withdraw` — a leave request is withdrawn * `leave_request_destroy` — a leave request is deleted If the leave policy has file attachments enabled or required, the attached files are not included in the webhook payload. ## Leave request created [#leave-request-created] Triggered instantly, as soon as the leave request is created. Action: `leave_request_create`. ```json { "action": "leave_request_create", "data": { "id": 1062480, "attributes": { "employee_id": 2632, "starts_on": "2024-01-31", "ends_on": "2024-02-01", "amount": "2.0", "description": "Vacation with family", "unit": "days", "state": "approved" }, "entries": [ { "occurs_on": "2024-01-31", "amount": "1.0" }, { "occurs_on": "2024-02-01", "amount": "1.0" } ], "leave_type": { "id": 13289, "name": "Vacation" }, "meta": { "created_at": "2024-02-01T15:43:50.841+00:00", "updated_at": "2024-02-01T15:43:50.878+00:00" } } } ``` ## Leave request approved [#leave-request-approved] Triggered instantly, as soon as the last approver in the approval flow approves the request and its status changes to Approved. Action: `leave_request_approve`. ```json { "action": "leave_request_approve", "data": { "id": 345842, "attributes": { "employee_id": 2632, "starts_on": "2022-09-20", "ends_on": "2022-09-23", "amount": "32.0", "description": "", "unit": "hours", "state": "approved" }, "entries": [ { "occurs_on": "2022-09-20", "amount": "8.0" }, { "occurs_on": "2022-09-21", "amount": "8.0" }, { "occurs_on": "2022-09-22", "amount": "8.0" }, { "occurs_on": "2022-09-23", "amount": "8.0" } ], "leave_type": { "id": 4098, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:08:31.562+01:00", "updated_at": "2022-09-21T16:08:36.540+01:00" } } } ``` ## Leave request rejected [#leave-request-rejected] Triggered instantly, as soon as at least one approver in the approval flow rejects the request and its status changes to Rejected. Action: `leave_request_reject`. ```json { "action": "leave_request_reject", "data": { "id": 345847, "attributes": { "employee_id": 2632, "starts_on": "2022-09-06", "ends_on": "2022-09-11", "amount": "32.0", "description": "", "unit": "hours", "state": "rejected" }, "entries": [ { "occurs_on": "2022-09-06", "amount": "8.0" }, { "occurs_on": "2022-09-07", "amount": "8.0" }, { "occurs_on": "2022-09-08", "amount": "8.0" }, { "occurs_on": "2022-09-09", "amount": "8.0" }, { "occurs_on": "2022-09-10", "amount": "0.0" }, { "occurs_on": "2022-09-11", "amount": "0.0" } ], "leave_type": { "id": 4098, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:10:25.052+01:00", "updated_at": "2022-09-21T16:10:28.284+01:00" } } } ``` ## Leave request withdrawn [#leave-request-withdrawn] Triggered instantly, as soon as the leave request is withdrawn and its status changes to Withdrawn. Action: `leave_request_withdraw`. ```json { "action": "leave_request_withdraw", "data": { "id": 345850, "attributes": { "employee_id": 2632, "starts_on": "2022-09-28", "ends_on": "2022-09-30", "amount": "3.0", "description": "", "unit": "days", "state": "withdrawn" }, "entries": [ { "occurs_on": "2022-09-28", "amount": "1.0" }, { "occurs_on": "2022-09-29", "amount": "1.0" }, { "occurs_on": "2022-09-30", "amount": "1.0" } ], "leave_type": { "id": 3916, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:12:48.034+01:00", "updated_at": "2022-09-21T16:13:13.260+01:00" } } } ``` # Other webhooks (/company/v2/webhooks/other-webhooks) ## Workflow triggered [#workflow-triggered] This webhook can only be configured as an action inside a workflow. The payload carries the full employee record the workflow acts on. ```json { "data": { "id": 6369, "attributes": { "employee_number": null, "hired_on": "2017-08-21", "first_name": "Scott", "middle_name": "", "last_name": "Pilgrim", "email": "scott@example.com", "termination_effective_date": null, "termination_reason": null, "termination_type": null }, "reporting_to": null, "position": null, "department": { "id": 105328, "name": "Corporate" }, "division": null, "location": { "id": 3134, "name": "Spain" }, "custom_fields": { "a25b06fb-bcfb-4aac-b695-6aff138fac36": { "name": "HR manager", "value": null, "group": "Personal" } }, "meta": { "created_at": "2020-02-03T12:08:03.934+01:00", "updated_at": "2025-10-20T15:42:42.928+02:00" } } } ``` ## Survey launched [#survey-launched] Triggered instantly when a survey moves from "Scheduled" to "Running". Action: `survey_start`. ```json { "action": "survey_start", "data": { "id": 10441, "attributes": { "name": "Stress management", "state": "running", "locale": "en", "starts_at": "2024-02-01T00:00:00.000Z", "ends_at": "2024-02-29T00:00:00.000Z" }, "meta": { "created_at": "2024-02-01T16:32:17.956Z", "updated_at": "2024-02-01T16:34:00.563Z" } } } ``` # Overtime requests (/company/v2/webhooks/overtime-requests) PeopleForce offers four webhook topics covering the overtime-request lifecycle: * `overtime_request_create` — an overtime request is created * `overtime_request_update` — an overtime request is updated * `overtime_request_approve` — an overtime request is approved * `overtime_request_destroy` — an overtime request is deleted ## Overtime request created [#overtime-request-created] Triggered instantly, as soon as the overtime request is created. Action: `overtime_request_create`. ```json { "action": "overtime_request_create", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T20:30:00.000+00:00", "minutes": 150, "comment": "Release deployment support", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:12:03.841+00:00" } } } ``` ## Overtime request updated [#overtime-request-updated] Triggered instantly, as soon as the overtime request is updated. Action: `overtime_request_update`. ```json { "action": "overtime_request_update", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:20:47.116+00:00" } } } ``` ## Overtime request approved [#overtime-request-approved] Triggered instantly, as soon as the last approver in the approval flow approves the request and its status changes to Approved. Action: `overtime_request_approve`. ```json { "action": "overtime_request_approve", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "approved" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T10:05:12.298+00:00" } } } ``` ## Overtime request deleted [#overtime-request-deleted] Triggered instantly, as soon as the overtime request is deleted. Action: `overtime_request_destroy`. ```json { "action": "overtime_request_destroy", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:12:03.841+00:00" } } } ``` # Authentication (/company/v3/getting-started/authentication) PeopleForce authenticates API requests with an API key. There are **two types of key** depending on your use case — choose the one that matches your goal. | Key type | Access | Where it can be used | | :------------------ | :----------------------------------------------------------------------------- | :------------------------------------------------------------------------ | | **Company API key** | Full access to the PeopleForce API and people data, with minimal restrictions. | Server-to-server integrations only. **Never** on a public-facing website. | | **Career API key** | Limited to non-sensitive, vacancy-specific data. | Safe to use on a public-facing website (e.g. a custom careers page). | To create a key, go to **Settings → API keys** (at the bottom of the page) → **Generate API key**. ## Company API key [#company-api-key] A Company API key lets the holder retrieve or change data in your company account through the API. Use it to build any system integration to PeopleForce. A Company API key grants access to nearly all data in PeopleForce. Only use it in trusted server-to-server integrations, never on a public-facing website, and only share it with developers you trust. You can scope a Company key with these restrictions: * **People compensation** — limit viewing or editing of people compensation. * **Vacancy salary range** — limit viewing or editing of vacancy salary ranges. * **Candidate desired salary** — limit viewing or editing of candidate desired salary. * **Candidate sensitive fields** — limit viewing or editing of candidate sensitive fields. * **IP addresses** — restrict callers to an allow-list of IP addresses (e.g. office or home networks). Generating a Company API key ## Career API key [#career-api-key] A Career API key lets you retrieve vacancies from PeopleForce to build a custom careers page on your own website. Because it is limited to non-sensitive, vacancy-specific information, it is safe to embed in a public-facing site. See [Own career site integration](/careers/v1/guides/own-career-site) for how to use it. Generating a Career API key ## Using a key [#using-a-key] Pass the key in a request header named **`X-API-KEY`**: ```bash curl https://app.peopleforce.io/api/public/v3/employees \ -H "X-API-KEY: " ``` All API requests must be made over HTTPS — calls over plain HTTP will fail, and so will requests without authentication. Passing the API key in the X-API-KEY header ## Disabling a key [#disabling-a-key] From the API key list page you can **disable** a key to temporarily stop it from working without deleting it — useful while pausing an integration. The API keys list ## Revoking a key [#revoking-a-key] If you no longer need a key, delete it from **Settings → API keys** → find the key → **Delete**. Deleting an API key from the list Deleting an API key is permanent and immediate. Any integration using that key stops working at once, and the key cannot be recreated. Account for every integration before deleting. ## Troubleshooting [#troubleshooting] ### 401 Unauthorized [#401-unauthorized] Double-check that the API key was copied correctly and try again. ```json { "message": "Bad Credentials" } ``` ### 403 Forbidden [#403-forbidden] Your role doesn't have permission for this action — most often because the key is restricted from compensation data. ### 404 Not Found [#404-not-found] The resource could not be found. Check that your request refers to an existing object. ### 422 Unprocessable Entity [#422-unprocessable-entity] One or more fields failed validation. The response body lists every error: ```json { "success": false, "errors": [ "Field name can't be blank" ] } ``` ### 500 Internal Server Error [#500-internal-server-error] A problem on our side. Try again later, or [contact support](mailto:support@peopleforce.io). # FAQ (/company/v3/getting-started/faq) ## Is using the API free? [#is-using-the-api-free] Yes — the API is free for all PeopleForce clients, and we encourage everyone to use it. ## What are the API rate limits? [#what-are-the-api-rate-limits] The current limit is **300 requests per minute**. See [Rate limits](../api-basics/rate-limit.mdx) for details. ## I don't see Webhooks, API, or Settings in PeopleForce [#i-dont-see-webhooks-api-or-settings-in-peopleforce] These pages are only visible to admin users, or users with the webhook or API permission. Contact your PeopleForce administrator to request access. ## I found a bug / I got a 500 error [#i-found-a-bug--i-got-a-500-error] Sorry to hear it — we aim for the best quality in our product. If you hit an issue, [let us know](mailto:support@peopleforce.io) and we'll work on it. A `500` response automatically notifies our team, but you're still welcome to write in and describe what happened — it helps us resolve it faster, and we'll let you know once there's a fix. ## I couldn't find the endpoint or data I was looking for [#i-couldnt-find-the-endpoint-or-data-i-was-looking-for] We cover a wide range of cases across the API and webhooks, but maybe yours is new to us. [Let us know](mailto:support@peopleforce.io) and we'll do our best to help — see also [Request a feature](./request-a-feature.mdx). # Introduction (/company/v3/getting-started) You can use the PeopleForce Company API to access your PeopleForce data — retrieving information about the entities stored in your account and performing actions on them. The API is organised around REST: predictable resource-oriented URLs, JSON request and response bodies, and standard HTTP verbs and status codes. For example, you can sync employees into PeopleForce from an external system, keep your lists of departments, divisions, and positions up to date, read time-off balances, or pull engagement-survey results. You'll need an API key first; see [Authentication](./authentication.mdx) to generate one. Then browse the full endpoint list under **API reference** in the sidebar — every operation has a live "Send" panel. ## Explore the platform's capabilities [#explore-the-platforms-capabilities] * **[Authentication](./authentication.mdx)** — generate an API key and authenticate every request. * **[Webhooks](../webhooks/index.mdx)** — subscribe to events and get notified the moment data changes in PeopleForce. Use the version switcher at the top of the sidebar to move between API versions. This page applies to every version. If you have any issues or questions, [contact us](mailto:support@peopleforce.io). # Request a feature (/company/v3/getting-started/request-a-feature) Didn't find what you were looking for in the API or webhooks? Let us know. We keep the API broad and already cover the common use cases — but if something is missing, send us a feature request and we'll see whether it can be added. [Open the requests board →](https://feedback.peopleforce.io/b/for-devs) # Starting with webhooks (/company/v3/getting-started/starting-with-webhooks) Webhooks let external services be notified when certain events happen in PeopleForce. When a subscribed event occurs, PeopleForce sends a `POST` request to each URL you configure. ## Setting up a webhook [#setting-up-a-webhook] Go to **Settings → Webhooks → Add new webhook** and fill in four fields: * **Name** — a label so you can find the webhook later. * **Payload URL** — the server that receives the webhook `POST` requests. It must use a valid SSL certificate from a publicly trusted CA. * **Secret** — an optional string used to sign requests (see below). * **Topics** — the events you want to be notified about; you can choose multiple. Save the webhook and it's live. See the full list of events in the [Webhooks overview](../webhooks/index.mdx). Creating a new webhook in Settings ## Secrets [#secrets] A secret is a shared string used to authenticate webhook deliveries. If you set one, PeopleForce signs each request and adds an `x-peopleforce-signature` header. Without the secret, no one else can forge a request with a matching signature. To verify the signature: * It is computed with **HMAC-SHA256** over the raw request body, keyed with your secret. * The result is a hexadecimal digest, prefixed with `sha256=`. * Compare it against the `x-peopleforce-signature` header using a **constant-time** comparison to avoid timing attacks. * Treat the payload as UTF-8 encoded text. ```ruby title="Ruby" def verify_signature(payload_body, signature_header) secret_key = ENV['WEBHOOK_SECRET'] computed_signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret_key, payload_body) unless Rack::Utils.secure_compare(computed_signature, signature_header) return halt 500, "Signatures didn't match!" end end ``` ```python title="Python" import hmac import hashlib import os def verify_signature(payload_body, signature_header): secret_token = os.getenv('WEBHOOK_SECRET') if not signature_header: raise HTTPException(status_code=403, detail="Missing signature header!") hash_object = hmac.new(key=secret_token.encode('utf-8'), msg=payload_body.encode('utf-8'), digestmod=hashlib.sha256) expected_signature = 'sha256=' + hash_object.hexdigest() if not hmac.compare_digest(expected_signature, signature_header): raise HTTPException(status_code=403, detail="Signatures did not match!") ``` ## Testing a webhook [#testing-a-webhook] When an event fires, PeopleForce delivers the JSON payload as the body of the `POST` request. To try it out, point the Payload URL at a request inspector such as [webhook.site](https://webhook.site/) (external link — be mindful of your data), then trigger the event. For a new employee, the payload looks like: ```json { "action": "employee_create", "data": { "id": 130333, "attributes": { "employee_number": "PF124593", "hired_on": "2022-09-12", "probation_ends_on": "2022-12-12", "first_name": "John", "last_name": "Doe", "email": "john@peopleforce.io", "personal_email": null, "gender": "male", "mobile_number": "", "work_phone_number": "", "date_of_birth": "1978-08-31", "termination_effective_date": null, "termination_comment": null, "avatar_url": null }, "reporting_to": { "id": 5844, "full_name": "Ross Kate", "email": "kate@peopleforce.io" }, "employment_type": { "id": 1899, "name": "Full-Time" }, "position": { "id": 11943, "name": "Senior Developer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": null, "custom_fields": { "2692d87e-c388-49ea-b903-616bc1557746": { "name": "T-shirt size", "value": "M", "group": "Personal" } }, "meta": { "created_at": "2022-09-05T18:33:55.130+03:00", "updated_at": "2022-09-05T18:33:55.526+03:00" } } } ``` Webhook payloads are emitted by the PeopleForce platform and use the platform's own field names — they are independent of the REST API version you call. ## Execution history [#execution-history] To confirm a delivery, go to **Settings → Webhooks** and click a webhook's name to see its execution history. Click any delivery to view the raw data that was sent. Webhook execution history ## Troubleshooting [#troubleshooting] If a delivery didn't arrive or didn't look right: 1. Check the Payload URL is correct and the receiving server supports webhooks. 2. Check the webhook's run history. If it was delivered, re-check step 1, then step 3. 3. Make sure your receiving server is up and healthy. | Status | Meaning | | :---------------------------- | :------------------------------------------------------------------------------------------- | | **200 Success** | Webhook was delivered. | | **404 Not Found** | The resource could not be found — check it refers to an existing object. | | **500 Internal Server Error** | A problem on our side — try again later or [contact support](mailto:support@peopleforce.io). | If none of these help, [contact us](mailto:support@peopleforce.io). # Adding a new hire from another ATS (/company/v2/guides/ats-new-hire) When you hire someone in an external ATS and want them in PeopleForce without extra manual work, create them through the Company API. **Prerequisite:** an API key. If you don't have one, start with [Authentication](../getting-started/authentication.mdx). ## Creating an employee via the API [#creating-an-employee-via-the-api] An employee profile is the account an employee uses to log in. It stores their core information — name, date of birth, email, phone number, hire date, and more. Create an employee with `POST /employees`: ```bash curl -X POST https://app.peopleforce.io/api/public/v2/employees \ -H "X-API-KEY: " \ -H "Content-Type: application/json" \ -d '{ "first_name": "Andrew", "last_name": "Doe", "email": "andrew@example.com", "hired_on": "2024-07-01" }' ``` You can also provide middle name, work and personal email, hire date, position, department, division, manager ID, and any custom field. See **Create an employee** in the API reference for the full field list. ### Custom fields [#custom-fields] Address a custom field by its identifier. In **v1** custom fields are keyed by the field's **UUID**; from **v2** onward they're keyed by the generated **`internal_name`**. Look up the identifier with the **List employee fields** endpoint. ```json { "first_name": "Andrew", "tax_number": "123123" } ``` ## Troubleshooting [#troubleshooting] **Validation errors (422).** Check the response message — you may be missing a required field or sending invalid/duplicate data. Personal and work emails and the personal phone number **must be unique**; first and last name are required. If the employee already exists you'll get a `422`. Look them up by email with **List employees** (`GET /employees`), then update them with `PUT /employees/{id}`. If your ATS can't call an API directly, check whether it integrates with [Zapier](https://zapier.com/apps/peopleforce/integrations) — Zapier can sit in between, receive your new hire, and call the PeopleForce API for you. # ERP employee directory integration (/company/v2/guides/erp-directory) This guide covers the endpoints useful for pushing data into PeopleForce from an external ERP: how to set up your structural lists, create employee records, and keep them up to date. It's a starting point — for anything not covered, browse the **API reference** in the sidebar. **Prerequisite:** an API key. If you don't have one, start with [Authentication](../getting-started/authentication.mdx). ## Step 1. Prepare your structural lists [#step-1-prepare-your-structural-lists] Before creating employees, set up the org data their profiles reference. Create these in the PeopleForce UI or via the API. | Resource | Endpoint | Notes | | :--------------- | :----------------------- | :------------------------------------------------------------------------------------ | | Locations | `POST /locations` | Your offices or places of operation. | | Divisions | `POST /divisions` | Larger business units. | | Departments | `POST /departments` | Main structural units; can be nested — create parents first, then pass the parent ID. | | Employment types | `POST /employment_types` | Full-time, part-time, contractor, etc. | | Positions | `POST /positions` | Titles assigned to employees. | Already have these lists? Use the matching `GET` endpoints to fetch their IDs (which you'll need when creating employees), and the `PUT` endpoints to update them. ## Step 2. Create employees [#step-2-create-employees] ### Create the employee record [#create-the-employee-record] Create an employee with `POST /employees` — see [Adding a new hire from another ATS](./ats-new-hire.mdx) for the request shape. Fetch existing employees with `GET /employees` and update with `PUT /employees/{id}`. ### Add an employment status [#add-an-employment-status] `POST /employees/{id}/employment_statuses` records work schedule, employment type, probation, and effective date. Add a **new record** for each change to preserve history. ### Add a position [#add-a-position] `POST /employees/{id}/positions` records the employee's position, department, division, location, and manager. Add a **new record** for each change (e.g. a promotion). ### Add compensation [#add-compensation] `POST /employees/{id}/salary` records salary, frequency, currency, and effective date. Add a **new record** for each change. ## Step 3. Leave balances [#step-3-leave-balances] Receiving leave balances from an external system is **disabled by default** for security and enabled per request — [contact us](mailto:support@peopleforce.io) to set it up. The flow involves creating leave types and policies and assigning policies to employees; you can also subscribe to [leave-request webhooks](../webhooks/leave-requests.mdx) to react to changes. # Job boards integration (/company/v2/guides/job-boards) This guide is for job board platforms (e.g. Indeed-style sites where many companies post openings) that want to integrate with PeopleForce using an API key. It covers the endpoints, workflow, and best practices for syncing job postings and candidate applications. Building a careers page for a single company's own website instead? See [Own career site integration](/careers/v1/guides/own-career-site). **Endpoint availability.** This workflow relies on PeopleForce recruitment endpoints (vacancies and candidates). These are **not yet part of the public Company API v4 surface** documented here. Confirm the available endpoints and base path with the PeopleForce team before building. The workflow below is preserved from the previous integration guide. ## Integration workflow [#integration-workflow] The customer's essential tasks are: * Publish a job from PeopleForce to your platform. * Receive candidate applications and sync them back to PeopleForce. Suggested setup flow: 1. Add a settings field where customers paste their PeopleForce API key. 2. When a customer creates a job posting, let them pick from their open PeopleForce vacancies and pre-fill the details. 3. After publishing, route applicant data back to the correct PeopleForce vacancy and mark the source. ## Getting started: API key [#getting-started-api-key] 1. In PeopleForce, go to **Settings → Security → API keys**. 2. Generate a new key and store it securely. 3. See [Authentication](../getting-started/authentication.mdx) for details. One API key can be used across multiple accounts of the same customer (e.g. different departments or business units). ## Importing job data [#importing-job-data] 1. Prompt the customer to connect their account with an API key. 2. When creating a vacancy on your side, suggest selecting from existing PeopleForce vacancies. 3. Retrieve open vacancies and display those in a published state (`state=opened`). 4. Fetch the selected vacancy's details and import them (title, description, recruiter, salary, etc.). ### Vacancy data mapping [#vacancy-data-mapping] Either auto-fill your job form from the imported vacancy (requires pre-mapping fields between systems) or let customers map fields manually. ## Submitting candidates [#submitting-candidates] When a candidate applies: 1. Collect their information (name, email, phone, CV file, cover letter, etc.). 2. Send it to PeopleForce, linked to the correct vacancy. 3. Include a `source` value (e.g. your platform name) for reporting. 4. Use the applications parameter to assign the candidate to the right vacancy stage. 5. If the same candidate applies to another vacancy at the same company, update the existing candidate (by ID) and add another entry to its applications. ### Candidate de-duplication [#candidate-de-duplication] * PeopleForce auto-checks for duplicates by email and CV file. * If a duplicate is found, the candidate's information is updated. * Manual updates via the UI are also possible. ## Test environment [#test-environment] 1. Request access to the PeopleForce test environment. 2. Use test data to simulate job sync and candidate submissions. 3. When complete, schedule a live demo with the PeopleForce team and share sandbox access for QA. ## Best practices [#best-practices] * Validate the API key before initiating syncs. * Clearly mark PeopleForce-originated jobs in your interface. * Require applications via your forms (not email or redirects) to capture complete data. * Log API errors and retry failed requests automatically. For test access, technical support, or feedback, contact us via [peopleforce.io/partners](https://peopleforce.io/partners). # Using PeopleForce data in user-facing solutions (/company/v2/guides/user-facing-data) Many customers surface PeopleForce data inside user-facing solutions — for example: * Showing a list of employees * Retrieving leave requests * Showing directory data such as positions or departments * Displaying vacancies on a website When you build this, implement it carefully to avoid exposing employee data and creating a security risk. ## Showing vacancies [#showing-vacancies] For displaying vacancies on a public site, use the dedicated **Career API key** and the Careers API — see [Own career site integration](/careers/v1/guides/own-career-site). The Career key is limited to non-sensitive vacancy data, so it's safe in a browser. ## Everything else: never expose the API key [#everything-else-never-expose-the-api-key] For all other data, **never put a Company API key in the browser (frontend)**. Protect it behind a backend proxy service. This matters even for internal, non-public solutions — a key exposed to your own team is still a data-security risk. A good pattern is a **two-tier architecture**: the frontend talks only to your backend, and your backend holds the API key and calls PeopleForce. 1. The user-facing app requests data from your backend — it never references a PeopleForce API key. 2. The backend receives the request and calls PeopleForce, using the API key it holds server-side (optionally filtering by use case). 3. PeopleForce responds to the backend. 4. The backend reduces the response to only the fields the frontend needs, then returns it. This keeps the API key encapsulated in the backend, where it's never exposed to the client. Combine this with key restrictions — IP allow-lists and field-level limits on the Company API key (see [Authentication](../getting-started/authentication.mdx)) — for defence in depth. # Changelog (/company/v3/api-basics/changelog) This log records notable changes to the Company API — newest first. ## August 16, 2026 [#august-16-2026] * Candidate CV/resume download URLs (`resume_url`) are now short-lived, expiring signed URLs (60 minutes) instead of permanent, unauthenticated links. Fetch a fresh URL before each download rather than caching it. ## June 29, 2026 [#june-29-2026] * Added `created_at`/`updated_at` filters to [list vacancy applications](https://developer.peopleforce.io/reference/list-recruitment-candidates-applications). * Added the `overtime_request_approve` webhook topic, triggered when the last approver approves an overtime request. See [Overtime requests](../webhooks/overtime-requests.mdx). ## April 14, 2024 [#april-14-2024] * Introduced **secrets** for webhooks. See [Starting with webhooks](../getting-started/starting-with-webhooks.mdx#secrets). * Webhooks can now be enabled/disabled in the web application. * Added `cover_letter` to create/update recruitment candidates. * Added custom table data to the get-employee response. * Added endpoints for time projects and project fields on timesheet entries. ## March 14, 2024 [#march-14-2024] * Added endpoints to manage the company skill list and employee skills. ## March 3, 2024 [#march-3-2024] * Added paid/unpaid and working/non-working info to leave requests. * Added endpoints for employee avatar, certifications, documents, and tables. ## December 30, 2023 [#december-30-2023] * Added bulk create/destroy for timesheet entries. ## November 26, 2023 [#november-26-2023] * Added the API rate limit. See [Rate limits](./rate-limit.mdx). ## April 2, 2023 [#april-2-2023] * Introduced **API v2**: custom fields addressed by `internal_name` (replacing UUIDs); `group` returned as an object; added an `options` field for select fields. # Pagination (/company/v3/api-basics/pagination) The API uses pagination for its list endpoints. List responses return a `data` array alongside a `metadata` object with pagination details. The page size is **50 items**. ```json { "data": [], "metadata": { "page": 1, "pages": 7, "count": 340, "items": 50 } } ``` | Field | Description | | :------ | :-------------------------------------------------------------------------------- | | `page` | The current page. | | `pages` | Total number of pages. | | `count` | Total number of items. | | `items` | Number of items on this page (50 for every page except the last, which has 1–50). | Request a specific page with the `page` query parameter: ```bash curl "https://app.peopleforce.io/api/public/v3/employees?page=2" \ -H "X-API-KEY: " ``` To walk the full result set, request `page=1` and keep incrementing `page` until it reaches `pages`. # Rate limits (/company/v3/api-basics/rate-limit) The PeopleForce API applies a rate limit to all endpoints to prevent misuse, protect the platform, and handle incoming volume. The limit is calculated **per minute** and enforced **per API key** (it falls back to the requesting IP address when a request carries no API key). The current limit is **300 requests per minute**. ## Recovering from a rate limit [#recovering-from-a-rate-limit] When you exceed the limit, the endpoint returns HTTP `429 Too Many Requests`. On a `429`, slow down your request rate and wait before retrying. Check the **`Retry-After`** response header to learn when the limit resets — its value is the number of seconds remaining until the limit is cleared. A good practice is to read that header and pause your requests for that many seconds before trying again. # Candidates and vacancies (/company/v3/webhooks/candidates-and-vacancies) For PeopleRecruit users, these webhooks cover the recruitment lifecycle: * `applicant_create` — a candidate is created * `vacancy_offer_accept` — a candidate accepted and signed an offer * `vacancy_create` — a vacancy is created * `vacancy_application_create` — a candidate applied or was added to a vacancy ## Applicant created [#applicant-created] Triggered instantly when a new applicant is created. Action: `applicant_create`. Custom fields are keyed by their field ID, each with a `name` and `value`. ```json { "action": "applicant_create", "data": { "id": 1461719, "attributes": { "full_name": "Pavlo Skrypka", "position": null, "email": "pavlo@example.com", "phone_numbers": ["380948593490"], "urls": [] }, "custom_fields": { "70f2c0d7-559a-4066-be3a-1cc53e547c52": { "name": "Driving license", "value": null }, "c162e147-7c10-4bee-b53b-72f0183edbc2": { "name": "Special notes", "value": null }, "18700c50-a0ac-40e1-8e52-f636a64a3edc": { "name": "desired_salary", "value": null } }, "meta": { "created_at": "2022-09-21T16:17:10.410+01:00", "updated_at": "2022-09-21T16:17:10.416+01:00" } } } ``` ## Applicant offer accepted [#applicant-offer-accepted] Triggered instantly when an applicant accepts and signs an offer. Action: `vacancy_offer_accept`. The payload does not include the offer document. ```json { "action": "vacancy_offer_accept", "data": { "id": 7978, "attributes": { "accepted_at": "2022-09-21T16:20:21.035+01:00", "rejected_at": null, "viewed_at": "2022-09-21T16:20:17.229+01:00" }, "applicant": { "id": 1461719, "full_name": "Pavlo Skrypka", "email": "pavlo@example.io" }, "vacancy": { "id": 526, "title": "QA Tester", "status": "accepted" }, "meta": { "created_at": "2022-09-21T16:19:58.757+01:00", "updated_at": "2022-09-21T16:20:21.013+01:00" } } } ``` ## Vacancy created [#vacancy-created] Triggered instantly when a vacancy is created. Action: `vacancy_create`. The `description` is delivered as HTML. ```json { "action": "vacancy_create", "data": { "id": 56721, "attributes": { "name": "UX Designer", "description": "
" }, "custom_fields": { "478214e1-0b81-4048-a6a3-63d3088d1ee0": { "name": "Custom field", "value": "Custom text" } }, "meta": { "created_at": "2024-02-01T16:20:40.896+00:00", "updated_at": "2024-02-01T16:20:40.906+00:00" } } } ``` ## Vacancy application created [#vacancy-application-created] Triggered instantly when a candidate applies or is added to a vacancy. Action: `vacancy_application_create`. ```json { "action": "vacancy_application_create", "data": { "id": 3102839, "candidate": { "id": 2151205, "full_name": "Jasmin Doe", "email": null }, "vacancy": { "id": 56721, "title": "UX Designer" }, "meta": { "created_at": "2024-02-01T16:27:14.814+00:00", "updated_at": "2024-02-01T16:27:14.814+00:00" } } } ``` # Employee compensation (/company/v3/webhooks/employee-compensation) Alongside an employee's profile attributes, PeopleForce keeps separate records for **position** and **salary**. To preserve the history of salary reviews and other changes, add a new record rather than editing the existing one. The salary record emits these webhooks: * `employee_salary_create` — a salary record is added * `employee_salary_update` — a salary record is updated Both fire immediately for any employee, active or terminated. ## Employee salary created [#employee-salary-created] Action: `employee_salary_create`. ```json { "action": "employee_salary_create", "data": { "id": 80093, "attributes": { "effective_on": "2022-09-21", "amount": "45000.0", "per": "month", "currency_code": "UAH", "comment": "" }, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com" }, "meta": { "created_at": "2022-09-21T15:59:57.173+01:00", "updated_at": "2022-09-21T15:59:57.173+01:00" } } } ``` ## Employee salary updated [#employee-salary-updated] Action: `employee_salary_update`. The payload shape matches `employee_salary_create`, with the updated values. # Employee job profile (/company/v3/webhooks/employee-job-profile) Alongside an employee's profile attributes, PeopleForce keeps a **job profile** history for each employee. As with position and salary, add a new record rather than editing the existing one so the history of moves is preserved. The job profile record emits these webhooks: * `employee_job_profile_create` — a job profile record is added * `employee_job_profile_update` — a job profile record is updated Both fire immediately for any employee, active or terminated. ## When they fire [#when-they-fire] A delivery is sent when the record is saved through any of these paths: * the **Job profile** section of an employee's profile * the Public API (`POST` / `PATCH` on an employee's job profiles) * an approved employee change request that adds a job profile record — this emits `employee_job_profile_create` * compliance workforce-planning verification, which adds a record `employee_job_profile_update` fires only when the save changes at least one stored value. A request that submits identical values sends nothing. A failed save sends nothing. ## Employee job profile created [#employee-job-profile-created] Action: `employee_job_profile_create`. ```json { "action": "employee_job_profile_create", "data": { "id": 4, "attributes": { "effective_on": "2026-08-14" }, "employee": { "id": 6, "employee_number": "IT-1029", "first_name": "Charles", "last_name": "Lancaster", "email": "charles@example.com" }, "job_profile": { "id": 2, "name": "Senior Developer" }, "job_group": { "id": 1, "name": "Engineering" }, "meta": { "created_at": "2026-08-14T14:18:40.723Z", "updated_at": "2026-08-14T14:18:40.723Z" } } } ``` ## Employee job profile updated [#employee-job-profile-updated] Action: `employee_job_profile_update`. The payload shape matches `employee_job_profile_create`, with the updated values. ```json { "action": "employee_job_profile_update", "data": { "id": 2, "attributes": { "effective_on": "2026-08-01" }, "employee": { "id": 4, "employee_number": null, "first_name": "Alex", "last_name": "Johnstone", "email": "alex@example.com" }, "job_profile": { "id": 1, "name": "Developer" }, "job_group": { "id": 1, "name": "Engineering" }, "meta": { "created_at": "2026-08-14T14:15:40.159Z", "updated_at": "2026-08-14T14:18:09.037Z" } } } ``` ## Field reference [#field-reference] | Field | Type | Notes | | :----------------------------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------- | | `data.id` | integer | The job profile history record ID, not the job profile ID. | | `data.attributes.effective_on` | date | The date the record takes effect. | | `data.employee` | object | Employee identity: `id`, `employee_number`, `first_name`, `last_name`, and `email`. `employee_number` is `null` when unset. | | `data.job_profile` | object | The assigned job profile: `id` and `name`. | | `data.job_group` | object or null | The job group the profile belongs to: `id` and `name`. `null` if the profile has no job group. | | `data.meta` | object | `created_at` and `updated_at` for the history record. | # Employee position (/company/v3/webhooks/employee-position) Alongside an employee's profile attributes (name, hire date, and so on), PeopleForce keeps separate records for **position** and **salary**. To preserve the history of promotions and other changes, add a new record rather than editing the existing one. The position record emits these webhooks: * `employee_position_create` — a position record is added * `employee_position_update` — a position record is updated Both fire immediately for any employee, active or terminated. ## Employee position created [#employee-position-created] Action: `employee_position_create`. ```json { "action": "employee_position_create", "data": { "id": 224222, "attributes": { "effective_on": "2022-12-01" }, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com" }, "reporting_to": { "id": 10277, "first_name": "Pahney", "last_name": "Zhelezo" }, "position": { "id": 11943, "name": "Senior Developer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": { "id": 28693, "name": "Vinnytsia" }, "meta": { "created_at": "2022-09-21T14:57:29.151+01:00", "updated_at": "2022-09-21T14:57:29.151+01:00" } } } ``` ## Employee position updated [#employee-position-updated] Action: `employee_position_update`. The payload shape matches `employee_position_create`, with the updated values. # Employee profile (/company/v3/webhooks/employee-profile) An employee profile is the account of an employee. Employees may log in using their email or Active Directory username, so take care when changing those fields. The employee profile emits these webhooks: * `employee_create` — an employee profile is created * `employee_update` — an employee profile is updated * `employee_start` — an employee's first day * `employee_terminate` — an employee is terminated * `employee_termination_revert` — a termination is reverted (reactivation) Webhook payloads are emitted by the PeopleForce platform and use the platform's own field names (e.g. `position`, `employee_number`) — independent of the Company API v4 resource naming. ## Employee profile created [#employee-profile-created] Triggered instantly when a new employee profile is created. Action: `employee_create`. ```json { "action": "employee_create", "data": { "id": 133710, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-09-26", "probation_ends_on": "2022-12-26", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com", "personal_email": null, "gender": "male", "mobile_number": "", "work_phone_number": "", "date_of_birth": "1978-08-29", "termination_effective_date": null, "termination_comment": null, "avatar_url": null }, "reporting_to": { "id": 10277, "full_name": "Zhelezo Pahney", "email": "zp@peopleforce.io" }, "employment_type": { "id": 1899, "name": "Full-Time" }, "position": { "id": 114651, "name": "IT Support Engineer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": { "id": 28693, "name": "Vinnytsia" }, "custom_fields": { "a25b06fb-bcfb-4aac-b695-6aff138fac36": { "name": "HR manager", "value": "5844", "group": "Personal" } }, "meta": { "created_at": "2022-09-21T16:29:07.894+03:00", "updated_at": "2022-09-21T16:29:08.713+03:00" } } } ``` ## Employee profile updated [#employee-profile-updated] Triggered instantly when an existing employee profile is updated. Action: `employee_update`. The payload shape matches `employee_create`, with the updated field values. ## Employee first day [#employee-first-day] Triggered in two cases: 1. Instantly, if an employee is created with a hire date of today. 2. Every night at 01:00 (UTC) for all employees whose hire date is that day. Action: `employee_start`. The payload shape matches `employee_create`. ```json { "action": "employee_start", "data": { "id": 133710, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-09-12", "first_name": "John", "last_name": "Doe", "email": "john@peopleforce.io" }, "meta": { "created_at": "2022-09-05T15:33:55.130Z", "updated_at": "2022-09-05T15:33:55.526Z" } } } ``` ## Employee terminated [#employee-terminated] Triggered in two cases: 1. Instantly, if an employee is terminated with a past date. 2. Every night at 01:00 (UTC) for all employees whose termination date is that day. Action: `employee_terminate`. The payload shape matches `employee_create`, with `termination_effective_date` set and most relational fields `null`. ```json { "action": "employee_terminate", "data": { "id": 123512, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-07-26", "first_name": "John", "last_name": "Doe", "termination_effective_date": "2022-11-07", "termination_comment": "" }, "reporting_to": null, "position": null, "department": null, "division": null, "location": null, "meta": { "created_at": "2022-07-29T10:42:43.354+01:00", "updated_at": "2022-11-10T11:12:32.102+00:00" } } } ``` ## Termination reverted [#termination-reverted] Triggered when an employee's termination is reverted — either cancelled before the termination date, or a terminated employee is reactivated. Action: `employee_termination_revert`. The payload shape matches `employee_create`. There is no `employee_activate` webhook. Reactivation and un-termination are delivered as `employee_termination_revert`. # Employee custom table rows (/company/v3/webhooks/employee-table-rows) **Custom tables** are the repeating tables you define yourself on an employee's profile — certifications, equipment, dependants, and so on. Each row emits its own event, so an integration can stay in step without polling. The custom table row emits these webhooks: * `employee_table_row_create` — a row is added * `employee_table_row_update` — a row is changed * `employee_table_row_destroy` — a row is deleted Subscribe to each one independently. Selecting one does not subscribe you to the other two, and existing endpoints gain none of them until you add them. Every payload is self-contained: it carries the employee, the table, and every column with its current value, so you do not need a follow-up API call. Values are the current state only — there is no before-and-after comparison. ## When they fire [#when-they-fire] A delivery is sent when the row is saved or deleted through any of these paths: * the employee's profile * the Public API (`POST` / `PATCH` / `DELETE` on an employee's table rows) * an approved employee change request that adds a row — this emits `employee_table_row_create` One event is sent per changed row, so a change request touching three rows sends three deliveries. ### When they do not fire [#when-they-do-not-fire] These topics cover **custom** tables only. Rows in PeopleForce system tables — position, salary, employment history, and compliance tables — never emit them. Nothing is sent when: * the save changes no stored value, so an identical update is silent * the create, update, delete, or approval fails * a change request is submitted, rejected, or cancelled — only approval emits * a row is created by a **hire form or preboarding form** ## Employee custom table row created [#employee-custom-table-row-created] Action: `employee_table_row_create`. ```json { "action": "employee_table_row_create", "data": { "id": 88213, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" }, "table": { "id": 4471, "name": "Certifications", "internal_name": "certifications" }, "columns": [ { "id": 90114, "name": "Level", "internal_name": "level", "type": "EmployeeTableColumns::Text", "value": "Professional" }, { "id": 90115, "name": "Certified on", "internal_name": "certified_on", "type": "EmployeeTableColumns::Date", "value": null }, { "id": 90116, "name": "Score", "internal_name": "score", "type": "EmployeeTableColumns::Number", "value": "87" }, { "id": 90117, "name": "Renewable", "internal_name": "renewable", "type": "EmployeeTableColumns::CheckBox", "value": "1" }, { "id": 90118, "name": "Rating", "internal_name": "rating", "type": "EmployeeTableColumns::SingleSelect", "value": { "id": "3312", "name": "Excellent" } }, { "id": 90119, "name": "Topics", "internal_name": "topics", "type": "EmployeeTableColumns::MultipleSelect", "value": [ { "id": "3315", "name": "Ruby" }, { "id": "3316", "name": "Rails" } ] }, { "id": 90120, "name": "Mentor", "internal_name": "mentor", "type": "EmployeeTableColumns::Reference", "value": { "id": "133711", "name": "Grace Hopper" } }, { "id": 90121, "name": "Notes", "internal_name": "notes", "type": "EmployeeTableColumns::LongText", "value": "Renewal due next year" } ], "meta": { "created_at": "2026-08-17T09:41:12.204Z", "updated_at": "2026-08-17T09:41:12.204Z" } } } ``` ## Employee custom table row updated [#employee-custom-table-row-updated] Action: `employee_table_row_update`. The payload shape matches `employee_table_row_create`, with the row's values after the change. ## Employee custom table row deleted [#employee-custom-table-row-deleted] Action: `employee_table_row_destroy`. The payload shape matches `employee_table_row_create`, and carries the row's final values as they stood before deletion. ## Field reference [#field-reference] | Field | Type | Notes | | :----------------------------- | :------ | :------------------------------------------------------------------------------------------ | | `data.id` | integer | The row ID. | | `data.employee` | object | Employee identity: `id`, `employee_number`, `first_name`, `last_name`, and `email`. | | `data.table` | object | The custom table: `id`, `name`, and `internal_name`. | | `data.columns` | array | Every column on the table, in the order shown in the product. Deleted columns are excluded. | | `data.columns[].id` | integer | The column ID. | | `data.columns[].internal_name` | string | The stable API name. Prefer it over `name`, which admins can rename freely. | | `data.columns[].type` | string | The column type — see the table below. | | `data.columns[].value` | varies | The current value — see the table below. | | `data.meta` | object | `created_at` and `updated_at` for the row. | ### Column values by type [#column-values-by-type] Every column on the table appears in `columns`, whether or not it holds a value. An empty column is present with a `null` value. | `type` | `value` shape | Empty | | :------------------------------------- | :------------------------------------------ | :----- | | `EmployeeTableColumns::Text` | string | `null` | | `EmployeeTableColumns::Number` | string, e.g. `"87"` | `null` | | `EmployeeTableColumns::Date` | string, e.g. `"2026-08-14"` | `null` | | `EmployeeTableColumns::CheckBox` | string, `"1"` when ticked or `"0"` when not | `null` | | `EmployeeTableColumns::LongText` | string, HTML stripped to plain text | `null` | | `EmployeeTableColumns::SingleSelect` | `{ "id": "…", "name": "…" }` | `null` | | `EmployeeTableColumns::MultipleSelect` | array of `{ "id": "…", "name": "…" }` | `[]` | | `EmployeeTableColumns::Reference` | `{ "id": "…", "name": "…" }` — an employee | `null` | Four details to code against: * **Scalars are JSON strings, not JSON numbers or booleans.** A number column sends `"87"`, a date sends `"2026-08-14"`, and a checkbox sends `"1"` or `"0"`. Both write paths store the value as text and the payload returns it uncast, so parse rather than assume. Rows loaded by an older import may still hold a raw JSON number. * An **unticked** checkbox sends `"0"`, which is a real value, not an empty one. Only a checkbox that was never set sends `null`. * A multi-select is **always an array**, never `null`. An empty one is `[]`. * The `id` inside a select, multi-select, or reference value is a **string**, because that is how the row stores it. The surrounding `columns[].id` and `data.id` are integers. A reference value keeps its stored `id` even when the referenced employee can no longer be found, in which case `name` is `null`. ## Testing a payload [#testing-a-payload] The sample-payload endpoint (`GET /webhooks?key=`) does not yet cover the three custom table row topics and returns `204 No Content` for them. To see a real payload, subscribe an endpoint, change a row, then open the delivery in **Settings → Webhooks → delivery history**. # Webhooks overview (/company/v3/webhooks) Webhooks let you subscribe to events happening in PeopleForce. Rather than polling the API, you configure an endpoint and PeopleForce sends it an HTTP `POST` request whenever a subscribed event fires. You manage subscriptions — and review past deliveries and their payloads — in your PeopleForce **Settings**. See [Starting with webhooks](../getting-started/starting-with-webhooks.mdx) for how to create one, secure it with a signing secret, and test deliveries. Every delivery has the shape `{ "action": "", "data": { … } }`, where `action` is the topic name (e.g. `employee_create`). Doesn't your integration support receiving webhooks? Check whether it supports Zapier — PeopleForce offers a range of [triggers and actions for Zapier](https://zapier.com/apps/peopleforce/integrations). ## Available topics [#available-topics] PeopleForce offers the webhook topics below. Availability depends on the PeopleForce modules enabled for your account. ### Employee [#employee] | Topic | Description | | :---------------------------------------- | :---------------------------------------------------- | | `employee_create` | An employee is created. | | `employee_update` | An employee is updated. | | `employee_start` | An employee's first day (hire date is reached). | | `employee_terminate` | An employee is terminated. | | `employee_termination_revert` | An employee's termination is reverted (reactivation). | | `employee_position_create` | An employee position record is created. | | `employee_position_update` | An employee position record is updated. | | `employee_job_profile_create` | An employee job profile record is created. | | `employee_job_profile_update` | An employee job profile record is updated. | | `employee_table_row_create` | A row in an employee's custom table is created. | | `employee_table_row_update` | A row in an employee's custom table is updated. | | `employee_table_row_destroy` | A row in an employee's custom table is deleted. | | `employee_salary_create` | An employee salary record is created. | | `employee_salary_update` | An employee salary record is updated. | | `employee_additional_compensation_create` | An additional compensation is created. | | `employee_additional_compensation_update` | An additional compensation is updated. | | `employee_employment_status_create` | An employment status record is created. | | `employee_employment_status_update` | An employment status record is updated. | | `external_user_create` | An external user is created. | ### Leave [#leave] | Topic | Description | | :----------------------- | :---------------------------- | | `leave_request_create` | A leave request is created. | | `leave_request_approve` | A leave request is approved. | | `leave_request_reject` | A leave request is rejected. | | `leave_request_withdraw` | A leave request is withdrawn. | | `leave_request_destroy` | A leave request is deleted. | ### Recruitment [#recruitment] | Topic | Description | | :----------------------------- | :--------------------------------------------------------------------- | | `applicant_create` | A candidate is created. | | `applicant_destroy` | A candidate is deleted. | | `vacancy_create` | A vacancy is created. | | `vacancy_application_create` | A candidate applied or was added to a vacancy. | | `vacancy_application_movement` | An application moved pipeline stage, was disqualified, or requalified. | | `vacancy_offer_accept` | A vacancy offer was accepted by the candidate. | | `vacancy_offer_reject` | A vacancy offer was rejected by the candidate. | ### Time [#time] | Topic | Description | | :------------------------- | :------------------------------- | | `overtime_request_create` | An overtime request is created. | | `overtime_request_update` | An overtime request is updated. | | `overtime_request_destroy` | An overtime request is deleted. | | `overtime_request_approve` | An overtime request is approved. | ### Other [#other] | Topic | Description | | :------------- | :------------------------------------------------------ | | `survey_start` | A survey is launched (moves from Scheduled to Running). | A **workflow** webhook can also be configured as an *action* inside a workflow — it isn't part of the subscribable topic list above. See [Other webhooks](./other-webhooks.mdx). ## Delivery headers [#delivery-headers] Every delivery includes: * `Content-Type: application/json` * `X-PeopleForce-Endpoint` — the webhook endpoint ID * `X-PeopleForce-Delivery` — a unique delivery ID * `X-PeopleForce-Signature` — present only when the endpoint has a secret configured (see [signing](../getting-started/starting-with-webhooks.mdx#secrets)) Deliveries time out after 5 seconds and require a valid TLS certificate. The pages in this section group these topics and show example payloads: [Employee profile](./employee-profile.mdx), [Employee position](./employee-position.mdx), [Employee job profile](./employee-job-profile.mdx), [Employee custom table rows](./employee-table-rows.mdx), [Employee compensation](./employee-compensation.mdx), [Leave requests](./leave-requests.mdx), [Candidates and vacancies](./candidates-and-vacancies.mdx), [Overtime requests](./overtime-requests.mdx), and [Other webhooks](./other-webhooks.mdx). # Leave requests (/company/v3/webhooks/leave-requests) PeopleForce offers five webhook topics covering the leave-request lifecycle: * `leave_request_create` — a leave request is created * `leave_request_approve` — a leave request is approved * `leave_request_reject` — a leave request is rejected * `leave_request_withdraw` — a leave request is withdrawn * `leave_request_destroy` — a leave request is deleted If the leave policy has file attachments enabled or required, the attached files are not included in the webhook payload. ## Leave request created [#leave-request-created] Triggered instantly, as soon as the leave request is created. Action: `leave_request_create`. ```json { "action": "leave_request_create", "data": { "id": 1062480, "attributes": { "employee_id": 2632, "starts_on": "2024-01-31", "ends_on": "2024-02-01", "amount": "2.0", "description": "Vacation with family", "unit": "days", "state": "approved" }, "entries": [ { "occurs_on": "2024-01-31", "amount": "1.0" }, { "occurs_on": "2024-02-01", "amount": "1.0" } ], "leave_type": { "id": 13289, "name": "Vacation" }, "meta": { "created_at": "2024-02-01T15:43:50.841+00:00", "updated_at": "2024-02-01T15:43:50.878+00:00" } } } ``` ## Leave request approved [#leave-request-approved] Triggered instantly, as soon as the last approver in the approval flow approves the request and its status changes to Approved. Action: `leave_request_approve`. ```json { "action": "leave_request_approve", "data": { "id": 345842, "attributes": { "employee_id": 2632, "starts_on": "2022-09-20", "ends_on": "2022-09-23", "amount": "32.0", "description": "", "unit": "hours", "state": "approved" }, "entries": [ { "occurs_on": "2022-09-20", "amount": "8.0" }, { "occurs_on": "2022-09-21", "amount": "8.0" }, { "occurs_on": "2022-09-22", "amount": "8.0" }, { "occurs_on": "2022-09-23", "amount": "8.0" } ], "leave_type": { "id": 4098, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:08:31.562+01:00", "updated_at": "2022-09-21T16:08:36.540+01:00" } } } ``` ## Leave request rejected [#leave-request-rejected] Triggered instantly, as soon as at least one approver in the approval flow rejects the request and its status changes to Rejected. Action: `leave_request_reject`. ```json { "action": "leave_request_reject", "data": { "id": 345847, "attributes": { "employee_id": 2632, "starts_on": "2022-09-06", "ends_on": "2022-09-11", "amount": "32.0", "description": "", "unit": "hours", "state": "rejected" }, "entries": [ { "occurs_on": "2022-09-06", "amount": "8.0" }, { "occurs_on": "2022-09-07", "amount": "8.0" }, { "occurs_on": "2022-09-08", "amount": "8.0" }, { "occurs_on": "2022-09-09", "amount": "8.0" }, { "occurs_on": "2022-09-10", "amount": "0.0" }, { "occurs_on": "2022-09-11", "amount": "0.0" } ], "leave_type": { "id": 4098, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:10:25.052+01:00", "updated_at": "2022-09-21T16:10:28.284+01:00" } } } ``` ## Leave request withdrawn [#leave-request-withdrawn] Triggered instantly, as soon as the leave request is withdrawn and its status changes to Withdrawn. Action: `leave_request_withdraw`. ```json { "action": "leave_request_withdraw", "data": { "id": 345850, "attributes": { "employee_id": 2632, "starts_on": "2022-09-28", "ends_on": "2022-09-30", "amount": "3.0", "description": "", "unit": "days", "state": "withdrawn" }, "entries": [ { "occurs_on": "2022-09-28", "amount": "1.0" }, { "occurs_on": "2022-09-29", "amount": "1.0" }, { "occurs_on": "2022-09-30", "amount": "1.0" } ], "leave_type": { "id": 3916, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:12:48.034+01:00", "updated_at": "2022-09-21T16:13:13.260+01:00" } } } ``` # Other webhooks (/company/v3/webhooks/other-webhooks) ## Workflow triggered [#workflow-triggered] This webhook can only be configured as an action inside a workflow. The payload carries the full employee record the workflow acts on. ```json { "data": { "id": 6369, "attributes": { "employee_number": null, "hired_on": "2017-08-21", "first_name": "Scott", "middle_name": "", "last_name": "Pilgrim", "email": "scott@example.com", "termination_effective_date": null, "termination_reason": null, "termination_type": null }, "reporting_to": null, "position": null, "department": { "id": 105328, "name": "Corporate" }, "division": null, "location": { "id": 3134, "name": "Spain" }, "custom_fields": { "a25b06fb-bcfb-4aac-b695-6aff138fac36": { "name": "HR manager", "value": null, "group": "Personal" } }, "meta": { "created_at": "2020-02-03T12:08:03.934+01:00", "updated_at": "2025-10-20T15:42:42.928+02:00" } } } ``` ## Survey launched [#survey-launched] Triggered instantly when a survey moves from "Scheduled" to "Running". Action: `survey_start`. ```json { "action": "survey_start", "data": { "id": 10441, "attributes": { "name": "Stress management", "state": "running", "locale": "en", "starts_at": "2024-02-01T00:00:00.000Z", "ends_at": "2024-02-29T00:00:00.000Z" }, "meta": { "created_at": "2024-02-01T16:32:17.956Z", "updated_at": "2024-02-01T16:34:00.563Z" } } } ``` # Overtime requests (/company/v3/webhooks/overtime-requests) PeopleForce offers four webhook topics covering the overtime-request lifecycle: * `overtime_request_create` — an overtime request is created * `overtime_request_update` — an overtime request is updated * `overtime_request_approve` — an overtime request is approved * `overtime_request_destroy` — an overtime request is deleted ## Overtime request created [#overtime-request-created] Triggered instantly, as soon as the overtime request is created. Action: `overtime_request_create`. ```json { "action": "overtime_request_create", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T20:30:00.000+00:00", "minutes": 150, "comment": "Release deployment support", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:12:03.841+00:00" } } } ``` ## Overtime request updated [#overtime-request-updated] Triggered instantly, as soon as the overtime request is updated. Action: `overtime_request_update`. ```json { "action": "overtime_request_update", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:20:47.116+00:00" } } } ``` ## Overtime request approved [#overtime-request-approved] Triggered instantly, as soon as the last approver in the approval flow approves the request and its status changes to Approved. Action: `overtime_request_approve`. ```json { "action": "overtime_request_approve", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "approved" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T10:05:12.298+00:00" } } } ``` ## Overtime request deleted [#overtime-request-deleted] Triggered instantly, as soon as the overtime request is deleted. Action: `overtime_request_destroy`. ```json { "action": "overtime_request_destroy", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:12:03.841+00:00" } } } ``` # Adding a new hire from another ATS (/company/v3/guides/ats-new-hire) When you hire someone in an external ATS and want them in PeopleForce without extra manual work, create them through the Company API. **Prerequisite:** an API key. If you don't have one, start with [Authentication](../getting-started/authentication.mdx). ## Creating an employee via the API [#creating-an-employee-via-the-api] An employee profile is the account an employee uses to log in. It stores their core information — name, date of birth, email, phone number, hire date, and more. Create an employee with `POST /employees`: ```bash curl -X POST https://app.peopleforce.io/api/public/v3/employees \ -H "X-API-KEY: " \ -H "Content-Type: application/json" \ -d '{ "first_name": "Andrew", "last_name": "Doe", "email": "andrew@example.com", "hired_on": "2024-07-01" }' ``` You can also provide middle name, work and personal email, hire date, position, department, division, manager ID, and any custom field. See **Create an employee** in the API reference for the full field list. ### Custom fields [#custom-fields] Address a custom field by its identifier. In **v1** custom fields are keyed by the field's **UUID**; from **v2** onward they're keyed by the generated **`internal_name`**. Look up the identifier with the **List employee fields** endpoint. ```json { "first_name": "Andrew", "tax_number": "123123" } ``` ## Troubleshooting [#troubleshooting] **Validation errors (422).** Check the response message — you may be missing a required field or sending invalid/duplicate data. Personal and work emails and the personal phone number **must be unique**; first and last name are required. If the employee already exists you'll get a `422`. Look them up by email with **List employees** (`GET /employees`), then update them with `PUT /employees/{id}`. If your ATS can't call an API directly, check whether it integrates with [Zapier](https://zapier.com/apps/peopleforce/integrations) — Zapier can sit in between, receive your new hire, and call the PeopleForce API for you. # ERP employee directory integration (/company/v3/guides/erp-directory) This guide covers the endpoints useful for pushing data into PeopleForce from an external ERP: how to set up your structural lists, create employee records, and keep them up to date. It's a starting point — for anything not covered, browse the **API reference** in the sidebar. **Prerequisite:** an API key. If you don't have one, start with [Authentication](../getting-started/authentication.mdx). ## Step 1. Prepare your structural lists [#step-1-prepare-your-structural-lists] Before creating employees, set up the org data their profiles reference. Create these in the PeopleForce UI or via the API. | Resource | Endpoint | Notes | | :--------------- | :----------------------- | :------------------------------------------------------------------------------------ | | Locations | `POST /locations` | Your offices or places of operation. | | Divisions | `POST /divisions` | Larger business units. | | Departments | `POST /departments` | Main structural units; can be nested — create parents first, then pass the parent ID. | | Employment types | `POST /employment_types` | Full-time, part-time, contractor, etc. | | Positions | `POST /positions` | Titles assigned to employees. | Already have these lists? Use the matching `GET` endpoints to fetch their IDs (which you'll need when creating employees), and the `PUT` endpoints to update them. ## Step 2. Create employees [#step-2-create-employees] ### Create the employee record [#create-the-employee-record] Create an employee with `POST /employees` — see [Adding a new hire from another ATS](./ats-new-hire.mdx) for the request shape. Fetch existing employees with `GET /employees` and update with `PUT /employees/{id}`. ### Add an employment status [#add-an-employment-status] `POST /employees/{id}/employment_statuses` records work schedule, employment type, probation, and effective date. Add a **new record** for each change to preserve history. ### Add a position [#add-a-position] `POST /employees/{id}/positions` records the employee's position, department, division, location, and manager. Add a **new record** for each change (e.g. a promotion). ### Add compensation [#add-compensation] `POST /employees/{id}/salary` records salary, frequency, currency, and effective date. Add a **new record** for each change. ## Step 3. Leave balances [#step-3-leave-balances] Receiving leave balances from an external system is **disabled by default** for security and enabled per request — [contact us](mailto:support@peopleforce.io) to set it up. The flow involves creating leave types and policies and assigning policies to employees; you can also subscribe to [leave-request webhooks](../webhooks/leave-requests.mdx) to react to changes. # Job boards integration (/company/v3/guides/job-boards) This guide is for job board platforms (e.g. Indeed-style sites where many companies post openings) that want to integrate with PeopleForce using an API key. It covers the endpoints, workflow, and best practices for syncing job postings and candidate applications. Building a careers page for a single company's own website instead? See [Own career site integration](/careers/v1/guides/own-career-site). **Endpoint availability.** This workflow relies on PeopleForce recruitment endpoints (vacancies and candidates). These are **not yet part of the public Company API v4 surface** documented here. Confirm the available endpoints and base path with the PeopleForce team before building. The workflow below is preserved from the previous integration guide. ## Integration workflow [#integration-workflow] The customer's essential tasks are: * Publish a job from PeopleForce to your platform. * Receive candidate applications and sync them back to PeopleForce. Suggested setup flow: 1. Add a settings field where customers paste their PeopleForce API key. 2. When a customer creates a job posting, let them pick from their open PeopleForce vacancies and pre-fill the details. 3. After publishing, route applicant data back to the correct PeopleForce vacancy and mark the source. ## Getting started: API key [#getting-started-api-key] 1. In PeopleForce, go to **Settings → Security → API keys**. 2. Generate a new key and store it securely. 3. See [Authentication](../getting-started/authentication.mdx) for details. One API key can be used across multiple accounts of the same customer (e.g. different departments or business units). ## Importing job data [#importing-job-data] 1. Prompt the customer to connect their account with an API key. 2. When creating a vacancy on your side, suggest selecting from existing PeopleForce vacancies. 3. Retrieve open vacancies and display those in a published state (`state=opened`). 4. Fetch the selected vacancy's details and import them (title, description, recruiter, salary, etc.). ### Vacancy data mapping [#vacancy-data-mapping] Either auto-fill your job form from the imported vacancy (requires pre-mapping fields between systems) or let customers map fields manually. ## Submitting candidates [#submitting-candidates] When a candidate applies: 1. Collect their information (name, email, phone, CV file, cover letter, etc.). 2. Send it to PeopleForce, linked to the correct vacancy. 3. Include a `source` value (e.g. your platform name) for reporting. 4. Use the applications parameter to assign the candidate to the right vacancy stage. 5. If the same candidate applies to another vacancy at the same company, update the existing candidate (by ID) and add another entry to its applications. ### Candidate de-duplication [#candidate-de-duplication] * PeopleForce auto-checks for duplicates by email and CV file. * If a duplicate is found, the candidate's information is updated. * Manual updates via the UI are also possible. ## Test environment [#test-environment] 1. Request access to the PeopleForce test environment. 2. Use test data to simulate job sync and candidate submissions. 3. When complete, schedule a live demo with the PeopleForce team and share sandbox access for QA. ## Best practices [#best-practices] * Validate the API key before initiating syncs. * Clearly mark PeopleForce-originated jobs in your interface. * Require applications via your forms (not email or redirects) to capture complete data. * Log API errors and retry failed requests automatically. For test access, technical support, or feedback, contact us via [peopleforce.io/partners](https://peopleforce.io/partners). # Own career site integration (/company/v3/guides/own-career-site) This guide shows how to display your open PeopleForce vacancies on your own website. Whether you already have a careers page or are building one, you don't have to use the careers site provided by PeopleForce. The last section covers taking the application on your own site too. If you represent a job board and want to integrate your product with PeopleRecruit, see [Job boards integration](./job-boards.mdx) instead. ## Prerequisites [#prerequisites] Use the **Careers API** (`https://app.peopleforce.io/api/careers/v1`), which exposes only non-sensitive vacancy data. You'll need a **Career API key** — see [Authentication](../getting-started/authentication.mdx). Only ever use a **Career** API key on a public-facing website. A Company API key would expose worker data. ## Recommended flow [#recommended-flow] 1. Retrieve the list of vacancies from PeopleForce. 2. Retrieve filter data (locations and employment types) so candidates can filter. 3. Link each vacancy to the PeopleForce careers site to apply. ### 1. Retrieve the list of vacancies [#1-retrieve-the-list-of-vacancies] PeopleForce is the source of truth for your open vacancies. ```bash curl https://app.peopleforce.io/api/careers/v1/vacancies \ -H "X-API-KEY: " ``` This returns the list of open vacancies. ### 2. Retrieve locations and employment types [#2-retrieve-locations-and-employment-types] Use these to build drop-down filters; both can be used to filter the vacancies list: * `GET /api/careers/v1/locations` * `GET /api/careers/v1/employment_types` ### 3. Link out to apply [#3-link-out-to-apply] Building a robust application form (file upload, field validation, and so on) is complex, so we strongly recommend directing candidates to apply on the PeopleForce careers site. Each vacancy in the **List vacancies** and **Get a vacancy** responses includes an `apply_url` you can link to directly. If the candidate has to stay on your own domain, see [Accept applications on your own site](#accept-applications-on-your-own-site-server-side-integration) below. ## Accept applications on your own site (server-side integration) [#accept-applications-on-your-own-site-server-side-integration] To keep the form on your own domain, submit applications through the PeopleForce API instead of linking out. **This is a server-side integration.** It needs a **Company API key**, which grants full access to your account. The key must never reach the browser — not in JavaScript, not in a build artifact, not in a frontend environment variable. Your page posts to your own backend, and your backend calls PeopleForce. ### What you need [#what-you-need] * A **Company API key** — Settings → API keys → Generate API key. See [Authentication](/company/v3/getting-started/authentication). * A backend that can keep the key secret and hold an uploaded file long enough to forward it. * The vacancy `id`, which you already have from the Careers API vacancies list. The ids are the same across both APIs. ### How it works [#how-it-works] Submitting an application takes two calls: 1. **Create the candidate** — [`POST /api/public/v3/recruitment/candidates`](/company/v3/reference/candidates/create-recruitment-candidate) 2. **Attach the candidate to the vacancy** — [`POST /api/public/v3/recruitment/vacancies/{vacancy_id}/applications`](/company/v3/reference/vacancies/create-recruitment-vacancy-application) Treat the API reference as authoritative for the full field list and response schema. This guide covers what the reference does not: the order of the calls, the flags that matter, and the failure modes. Before building the form, fetch the custom fields your recruiters expect — see [Custom fields](#custom-fields). ### Step 1 — Create the candidate [#step-1--create-the-candidate] Reference: [Create a candidate](/company/v3/reference/candidates/create-recruitment-candidate) ```http POST https://app.peopleforce.io/api/public/v3/recruitment/candidates X-API-KEY: Content-Type: application/json ``` ```json { "full_name": "Ada Lovelace", "email": "ada@example.com", "phone_numbers": ["+44 12 3456 6789"], "cover_letter": "I am applying for the Senior Engineer role because…", "location": "London, UK", "urls": ["https://github.com/ada"], "source_id": 12, "consented_at": "2026-08-16T10:32:00Z", "resume": "data:application/pdf;name=ada-cv.pdf;base64,JVBERi0xLjQKJc..." } ``` Only `full_name` is required. A candidate with neither `email` nor `phone_numbers` is unusable to a recruiter, so require at least one contact method on your own form. | Field | Notes | | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `full_name` | **Required.** Max 50 characters. Cannot contain `` ` `` `!` `@` `#` `$` `%` `^` `&` `*` `+` `_` `=`. | | `email` | Must be valid, and must not already belong to another candidate — see [Handling duplicates](#handling-duplicates). | | `phone_numbers` | Array of strings. Digits, spaces, `(`, `)`, `+`, `.`, `-` only — no letters, no `x123` extensions. | | `cover_letter` | Plain text or HTML. | | `resume` | Base64 data URI — see [Uploading a CV](#uploading-a-cv). | | `consented_at`, `future_recruitment_consented_at` | ISO 8601 timestamps — see [Recording consent](#recording-consent). | | `source_id` | Where the application came from — see [Tracking the source](#tracking-the-source). | | `location`, `position`, `date_of_birth`, `skills`, `urls`, `telegram_username`, `desired_salary` + `currency_code`, `seniority_id`, `gender_id`, `tag_ids` | Optional. Some are hidden unless the key may see sensitive candidate fields. | Returns `201 Created`. Keep `data.id` for step 2. ```json { "data": { "id": 84213, "full_name": "Ada Lovelace", "email": "ada@example.com", "resume": true, "resume_url": "https://…", "applications": [], "custom_fields": [] } } ``` ### Step 2 — Attach the candidate to the vacancy [#step-2--attach-the-candidate-to-the-vacancy] Reference: [Create a vacancy application](/company/v3/reference/vacancies/create-recruitment-vacancy-application) ```http POST https://app.peopleforce.io/api/public/v3/recruitment/vacancies/{vacancy_id}/applications X-API-KEY: Content-Type: application/json ``` ```json { "applicant_id": 84213, "perform_automations": true } ``` **Pass `perform_automations: true`.** It defaults to off, and it runs the vacancy's pipeline automations — including the acknowledgement email to the candidate, if any are configured. Without it the application lands in the first pipeline stage and the candidate hears nothing. `applicant_state_id` starts the application in a specific pipeline stage instead of the first. Get the ids from [List pipelines](/company/v3/reference/vacancies/list-recruitment-pipelines). The vacancy must be **published**. A draft, closed, or archived vacancy returns `404`. ```json { "data": { "id": 55123, "applicant": { "id": 84213, "full_name": "Ada Lovelace", "email": "ada@example.com", "phone_numbers": ["+44 12 3456 6789"] }, "pipeline_state": { "id": 901, "name": "Applied" }, "created_at": "2026-08-16T10:32:04.000Z", "updated_at": "2026-08-16T10:32:04.000Z" } } ``` Applications created through the API do **not** notify the vacancy's collaborators and hiring lead, unlike the PeopleForce-hosted form. The candidate appears in the pipeline as normal and the `vacancy_application_create` [webhook](/company/v3/webhooks/candidates-and-vacancies) still fires — use that webhook if your team needs an immediate alert. ### Uploading a CV [#uploading-a-cv] `resume` takes the file inline as a base64 data URI, in exactly this shape: ```http data:;name=;base64, ``` Example: `data:application/pdf;name=ada-cv.pdf;base64,JVBERi0xLjQK...` Two things break, and neither says so clearly: * **The `;name=` segment is mandatory.** Without it the string is no longer recognised as a file at all — the API reads it as a signed attachment id and fails with `422` and `Invalid resume content`, which looks like a corrupt upload rather than a missing filename. * **Nothing may sit between the MIME type and `;name=`.** A `charset` parameter is swallowed into the type, which becomes `application/pdf;charset=utf-8`, matches nothing in the list below, and fails with `file type invalid`. Accepted MIME types: | Format | MIME type | | ------------------- | ------------------------------------------------------------------------- | | PDF | `application/pdf` | | Word (.docx) | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` | | Word (.doc) | `application/msword` | | OpenDocument (.odt) | `application/vnd.oasis.opendocument.text` | | RTF | `application/rtf`, `text/rtf`, `application/x-rtf`, `text/richtext` | The check reads the MIME type and the filename you declared — it does not look at the bytes — and the two have to agree. `data:application/pdf;name=cv.png` fails with `file type invalid` even though the type is an accepted one, so send the real type and the real extension. Enforce the same allowlist and a size limit on your own upload endpoint, so the candidate gets an immediate error instead of a round trip. PeopleForce parses the uploaded CV and enriches the candidate profile from it. For extra documents (portfolio, certificates) after the candidate exists, use [`POST /api/public/v3/recruitment/candidates/{candidate_id}/documents`](/company/v3/reference/candidates/create-recruitment-candidate-document) — a `multipart/form-data` upload in an `attachment` field. ### Custom fields [#custom-fields] Reference: [List candidate fields](/company/v3/reference/candidates/list-recruitment-candidate-fields) Fetch the custom candidate fields once, cache them, and render matching inputs on your form: ```http GET https://app.peopleforce.io/api/public/v3/recruitment/candidate_fields ``` ```json { "data": [ { "id": 41, "name": "Notice period", "internal_name": "notice_period", "type": "ApplicantFields::Select", "options": [ { "id": 501, "value": "Immediately" }, { "id": 502, "value": "1 month" } ] } ] } ``` Send them as **top-level keys** named by `internal_name`, not nested under a `custom_fields` object: ```json { "full_name": "Ada Lovelace", "email": "ada@example.com", "notice_period": 502 } ``` On `/api/public/v3`, select and multi-select fields take the option **`id`** (`502`, or `[502, 503]`). Other types take their plain value: text as a string, checkbox as a boolean, date as `YYYY-MM-DD`. A field marked required in PeopleForce is enforced here. Omitting it fails the whole candidate creation with `422`, so mirror the requirement on your form. ### Recording consent [#recording-consent] Hosting the form yourself means you own the GDPR consent checkboxes — PeopleForce cannot render them for you. Pass the timestamps through: * `consented_at` — consent to process the data for **this** application. * `future_recruitment_consented_at` — consent to stay on file for **future** vacancies. Both are ISO 8601 (`"2026-08-16T10:32:00Z"`). Send `future_recruitment_consented_at` only if that separate box was ticked — it drives data retention in PeopleForce. ### Tracking the source [#tracking-the-source] Reference: [List sources](/company/v3/reference/candidates/list-recruitment-sources) Candidates created through the API have no source unless you set one, which leaves them out of source-effectiveness reporting. Fetch the list once and cache the id: ```http GET https://app.peopleforce.io/api/public/v3/recruitment/sources ``` Pick the source for your site, or create one with `POST /api/public/v3/recruitment/sources` (`{"name": "Company website"}`), and pass its id as `source_id` on the candidate. If your career page also lands paid campaigns, keep reading the `source_id` query parameter you already use for the hosted form and map it to the matching PeopleForce source. ### Handling duplicates [#handling-duplicates] PeopleForce blocks duplicate candidates. Step 1 returns `422` when: * the **email** already belongs to another candidate; * a **phone number** already belongs to another candidate, if *Detect duplicates by phone number* is enabled in your recruiting settings; * the **same CV file** is already attached to another candidate. ```json { "success": false, "errors": ["Email already exists for Ada Lovelace"] } ``` A returning candidate is the normal case. Look them up ([List candidates](/company/v3/reference/candidates/list-recruitment-candidates)) and reuse them: ```http GET https://app.peopleforce.io/api/public/v3/recruitment/candidates?email=ada@example.com ``` The `email` filter matches on a **prefix**, so `ada@example.co` also returns `ada@example.com`. Pick the row whose `email` equals the address exactly — never just `data[0]` — then go to step 2 with its `id`. If that person has **already applied to this vacancy**, step 2 returns `422` with `"Applicant has already been taken"`. They are already in the pipeline, so show "you have already applied to this role" rather than an error. To refresh their CV and contact details with what they just submitted, call [`PUT /api/public/v3/recruitment/candidates/{id}`](/company/v3/reference/candidates/update-recruitment-candidate) before step 2. Looking a candidate up by **phone** instead? That filter takes a list, so it needs brackets in the query string: `?phone_numbers[]=%2B441234567890`. Without them the value arrives as a single string and the request fails with `400`. This applies to query strings only — in the JSON bodies above, a list is just a JSON array. ### Handling failures [#handling-failures] | Status | Meaning | What to do | | ------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `400` | A parameter has the wrong type | Read `errors` — each entry names the parameter and the type it expected | | `401` | Missing, wrong, or disabled API key | Check the `X-API-KEY` header. Do not retry. | | `403` | Key lacks permission, or its IP allowlist excludes your server | Add your server's egress IP to the key's allowlist. | | `404` | Vacancy is not published, or the candidate id is wrong | Refresh your vacancy list. | | `422` | Duplicate candidate, already applied, bad file type, or missing required custom field | Resolve as described above. Do not retry unchanged. | | `429` | Rate limited | Back off and retry, honouring `Retry-After`. | | `5xx` | Temporary server error | Retry with exponential backoff. | The dangerous state is **step 1 succeeded and step 2 failed**: a candidate with no application. Do not re-create the candidate on retry — retry step 2 with the id you already hold, and log that id so a failure outliving the request can be reconciled later. ### Rate limits [#rate-limits] The API follows the usual [rate limit](/company/v3/api-basics/rate-limit) rules — 300 requests a minute per API key. Honour `Retry-After` on a `429` and queue submissions instead of retrying in a tight loop. ### Security checklist [#security-checklist] Your form is a publicly writable endpoint into your recruiting database. The protections the hosted form provides are now yours to build: * **Keep the Company API key server-side.** Never proxy it to the browser, and never forward whatever the browser sends. * **Add bot protection** — reCAPTCHA, Turnstile, or equivalent — verified on your server before the PeopleForce calls. * **Rate-limit per visitor IP** on your own endpoint, independently of the PeopleForce limit. * **Validate uploads before forwarding**: extension, MIME type, maximum size. * **Never echo PeopleForce error messages verbatim.** A duplicate error is HTML and contains the existing candidate's name, linked to their profile, which discloses that they applied to you. * **Restrict the key by IP** in PeopleForce settings, to your server's egress addresses. ### Complete example [#complete-example] ```javascript // POST /apply const PF = "https://app.peopleforce.io/api/public/v3"; const headers = { "X-API-KEY": process.env.PEOPLEFORCE_API_KEY, "Content-Type": "application/json", }; async function findCandidateByEmail(email) { // Email is optional on the form, and a duplicate can be raised on phone or // CV instead — so this is reachable with nothing to look up. if (!email?.trim()) return null; const res = await fetch(`${PF}/recruitment/candidates?email=${encodeURIComponent(email)}`, { headers }); const body = await res.json(); // The filter is a prefix match, so "ada@example.co" also returns // "ada@example.com". Take the exact address, never just the first row. const wanted = email.trim().toLowerCase(); return body.data?.find((c) => c.email?.toLowerCase() === wanted)?.id ?? null; } async function createCandidate(form, file) { const payload = { full_name: form.fullName, email: form.email, phone_numbers: form.phone ? [form.phone] : [], cover_letter: form.coverLetter, source_id: Number(process.env.PEOPLEFORCE_SOURCE_ID), consented_at: form.consentedAt, // when the box was ticked notice_period: form.noticePeriodOptionId, // custom field, by internal_name }; if (file) { payload.resume = `data:${file.mimetype};name=${file.originalname};base64,${file.buffer.toString("base64")}`; } const res = await fetch(`${PF}/recruitment/candidates`, { method: "POST", headers, body: JSON.stringify(payload), }); if (res.status === 201) return (await res.json()).data.id; if (res.status === 422) { const { errors = [] } = await res.json(); // 422 is also a bad file type or a missing required custom field, so only // a duplicate is safe to recover from. The message is "… already exists // for " for email, phone and CV alike. if (!errors.some((e) => /already exists/i.test(e))) throw new ValidationError(errors); const existingId = await findCandidateByEmail(form.email); if (existingId) return existingId; // Duplicate phone or CV rather than email — the person exists, but not at // an address we can resolve them by. throw new ValidationError(errors); } throw new Error(`Candidate creation failed: ${res.status}`); } async function createApplication(vacancyId, candidateId) { const res = await fetch(`${PF}/recruitment/vacancies/${vacancyId}/applications`, { method: "POST", headers, body: JSON.stringify({ applicant_id: candidateId, perform_automations: true, }), }); if (res.status === 422) { // The only 422 this endpoint returns is the one-application-per-vacancy // rule — but check, rather than swallowing every validation error as // "already applied". const { errors = [] } = await res.json(); if (errors.some((e) => /already been taken/i.test(e))) return { alreadyApplied: true }; throw new ValidationError(errors); } if (res.status !== 201) { throw new Error(`Application failed for candidate ${candidateId}: ${res.status}`); } return (await res.json()).data; } app.post("/apply", upload.single("resume"), async (req, res) => { if (!(await verifyCaptcha(req.body.captchaToken))) { return res.status(400).render("apply", { error: "Please confirm you are not a robot." }); } let candidateId; try { candidateId = await createCandidate(req.body, req.file); const result = await createApplication(req.body.vacancyId, candidateId); if (result.alreadyApplied) return res.render("already-applied"); res.render("thank-you"); } catch (err) { // candidateId set but the application failed is the state worth logging: // a candidate with no application, to reconcile later. logger.error({ err, candidateId }, "career application submission failed"); if (err instanceof ValidationError) { // Your own wording — never PeopleForce's, which is HTML and can name an // existing candidate. return res.status(400).render("apply", { error: "Please check your details and try again." }); } res.status(500).render("apply", { error: "We could not submit your application. Please try again." }); } }); ``` The same calls with `curl`: ```bash # 1. Create the candidate curl -X POST https://app.peopleforce.io/api/public/v3/recruitment/candidates \ -H "X-API-KEY: $PEOPLEFORCE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "full_name": "Ada Lovelace", "email": "ada@example.com", "phone_numbers": ["+44 12 3456 6789"], "consented_at": "2026-08-16T10:32:00Z", "resume": "data:application/pdf;name=ada-cv.pdf;base64,JVBERi0xLjQK..." }' # 2. Attach them to the vacancy curl -X POST https://app.peopleforce.io/api/public/v3/recruitment/vacancies/1234/applications \ -H "X-API-KEY: $PEOPLEFORCE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "applicant_id": 84213, "perform_automations": true }' ``` ## Which approach should you choose? [#which-approach-should-you-choose] | | Link to `apply_url` | Your own form + API | | ----------------------------------------- | ------------------------------- | ----------------------------------------------- | | Effort | A hyperlink | A backend endpoint, file handling, error states | | API key | Career API key (publishable) | Company API key (**server-side only**) | | Candidate stays on your domain | No | Yes | | Bot protection | Built in | You build it | | File upload and validation | Built in | You build it | | GDPR consent UI | Built in, follows your settings | You build it | | Recruiter notification on new application | Yes | Webhook only | # Using PeopleForce data in user-facing solutions (/company/v3/guides/user-facing-data) Many customers surface PeopleForce data inside user-facing solutions — for example: * Showing a list of employees * Retrieving leave requests * Showing directory data such as positions or departments * Displaying vacancies on a website When you build this, implement it carefully to avoid exposing employee data and creating a security risk. ## Showing vacancies [#showing-vacancies] For displaying vacancies on a public site, use the dedicated **Career API key** and the Careers API — see [Own career site integration](/careers/v1/guides/own-career-site). The Career key is limited to non-sensitive vacancy data, so it's safe in a browser. ## Everything else: never expose the API key [#everything-else-never-expose-the-api-key] For all other data, **never put a Company API key in the browser (frontend)**. Protect it behind a backend proxy service. This matters even for internal, non-public solutions — a key exposed to your own team is still a data-security risk. A good pattern is a **two-tier architecture**: the frontend talks only to your backend, and your backend holds the API key and calls PeopleForce. 1. The user-facing app requests data from your backend — it never references a PeopleForce API key. 2. The backend receives the request and calls PeopleForce, using the API key it holds server-side (optionally filtering by use case). 3. PeopleForce responds to the backend. 4. The backend reduces the response to only the fields the frontend needs, then returns it. This keeps the API key encapsulated in the backend, where it's never exposed to the client. Combine this with key restrictions — IP allow-lists and field-level limits on the Company API key (see [Authentication](../getting-started/authentication.mdx)) — for defence in depth. # Changelog (/company/v4/api-basics/changelog) This page records notable changes to the PeopleForce APIs — new endpoints, fields, and behavioural changes — newest first. Company API **v4** is the current line and is under active development. Entries below track v4 changes. Older v1–v3 history from the previous documentation site is not carried over here. ## Unreleased [#unreleased] * Initial v4 documentation: Company API v4 and Careers API v1. # Pagination (/company/v4/api-basics/pagination) List endpoints are paginated. The response wraps its results in a `data` array alongside a `metadata` object describing the current page and the total result set. The pagination **shape changed in v4**. If you're migrating an integration built against an earlier version, see [Before v4](#before-v4) for the field mapping. ## v4 pagination [#v4-pagination] ```json { "data": [ // ... up to `per_page` resources ], "metadata": { "page": 1, "per_page": 50, "total_pages": 7, "total_count": 340 } } ``` ### Metadata fields [#metadata-fields] | Field | Description | | :------------ | :-------------------------------------- | | `page` | The current page number. | | `per_page` | Number of items returned per page. | | `total_pages` | Total number of pages available. | | `total_count` | Total number of items across all pages. | ### Query parameters [#query-parameters] | Parameter | Description | | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | | `page` | The page number to fetch. Defaults to `1`. | | `per_page` | Number of results per page. The default is set per endpoint — `50` for most lists (including People), `100` for a few. There is no enforced maximum. | | `offset` | Skip a number of results before the page starts. | ```bash curl "https://app.peopleforce.io/api/v4/people?page=2&per_page=100" \ -H "X-API-KEY: " ``` To walk the full result set, request `page=1` and keep incrementing `page` until it reaches `total_pages`. ### Response headers [#response-headers] The same pagination data is also returned as response headers, so you can paginate without parsing the body: `X-Total`, `X-Total-Pages`, `X-Per-Page`, `X-Page`, `X-Next-Page`, `X-Prev-Page`, `X-Offset`. ## Before v4 [#before-v4] Earlier API versions (v1–v3) used a different metadata object and a fixed page size of 50: ```json { "data": [], "metadata": { "page": 1, "pages": 7, "count": 340, "items": 50 } } ``` When migrating to v4, update these field names: | Pre-v4 | v4 | Notes | | :------ | :------------ | :--------------------------------------------------------------------------------------------------------------------- | | `pages` | `total_pages` | Total number of pages. | | `count` | `total_count` | Total number of items. | | `items` | `per_page` | Pre-v4 was the count of items on the current page (50 except the last page); v4 `per_page` is the requested page size. | | — | `page` | Unchanged — present in both. | v4 also adds a configurable `per_page` query parameter (pre-v4 was fixed at 50), the `offset` parameter, and the `X-*` pagination response headers. # Rate limits (/company/v4/api-basics/rate-limit) The PeopleForce API applies a rate limit to all endpoints to prevent misuse, protect the platform, and handle incoming volume. The limit is calculated **per minute** and enforced **per API key** (it falls back to the requesting IP address when a request carries no API key). The current limit is **300 requests per minute**. ## Recovering from a rate limit [#recovering-from-a-rate-limit] When you exceed the limit, the endpoint returns HTTP `429 Too Many Requests`. On a `429`, slow down your request rate and wait before retrying. Check the **`Retry-After`** response header to learn when the limit resets — its value is the number of seconds remaining until the limit is cleared. A good practice is to read that header and pause your requests for that many seconds before trying again. # Authentication (/company/v4/getting-started/authentication) API v4 authenticates with a **service account** API key, passed in the `X-API-KEY` header. Service accounts are new in v4 and are the only credential it accepts. The important difference from earlier versions: a service account key carries **no permissions of its own**. On its own it can read a little reference data and nothing else. Everything it can see and do comes from the **roles** you assign to it — the same Roles & permissions system that governs what your people can see in PeopleForce. Please note that API v4 currently has a subset of the endpoints available in API v3. We are currently adding new endpoints in every release. v4 accepts service account API keys only - you can't use a Company API key or a Career API key. Equally, a service account API key only works against v4 - you can't use it to access v3, v2 or v1 APIs. ## Why this changed [#why-this-changed] Company API keys have a very limited set of permissions for what can be restricted, and this is a separate mechanism from roles and permissions. As we seek to add more permissions to API keys, we decided to unify the approach to permissions between Web and API users. Service Account API keys represents a new direction, unifying the approach to permission management. ## Set up a service account [#set-up-a-service-account] ### 1. Create the key [#1-create-the-key] Go to **Settings → API keys → Generate API key**, name the key, and choose the **Service account** type ("Service account user for taking API actions using specific roles and permissions"). Copy the key immediately and store it in your secret manager — PeopleForce does not show it again. If **Service account** isn't offered as a key type, service accounts aren't enabled for your account yet. [Contact us](mailto:support@peopleforce.io) to have it turned on. ### 2. Create a matching role [#2-create-a-matching-role] Go to **Settings → Roles & permissions**, add a new role, and choose the **Service account** type. Give it a name describing the integration, for example `Payroll export` or `Marketing directory sync`. Service account roles are separate from the roles you assign to people. They appear in the same list and use the same permission model, but they can only be assigned to service account API keys. ### 3. Attach the key and scope the role [#3-attach-the-key-and-scope-the-role] The role form has three parts: | Section | What it controls | | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Who is assigned this role?** | The service account keys this role applies to. Select the key you created in step 1 — a role with no key attached grants nothing. | | **Company** tab | Company-wide actions: managing review cycles and the org-structure lists. These aren't tied to a population. | | **People** tab | First **Whose data can members access?** — the population, built from rules such as *Department is Marketing*. Then **What can this role see?** — the fields, records, and modules the role grants for those people. | ## What a role can grant [#what-a-role-can-grant] ### Company-wide actions [#company-wide-actions] Set these on the role's **Company** tab. Reading the org-structure lists needs no permission — any active service account key can `GET` them. The permissions below govern writes. Examples below - please see API Reference for full details. | Permission | Unlocks | | :----------------------- | :------------------------------------------------------------------------------------------------------------------ | | Manage departments | `POST` / `PUT` / `DELETE` on `/departments` | | Manage divisions | `POST` / `PUT` / `DELETE` on `/divisions` | | Manage locations | `POST` / `PUT` / `DELETE` on `/locations` | | Manage job titles | `POST` / `PUT` / `DELETE` on `/job_titles` | | Manage job levels | `POST` / `PUT` / `DELETE` on `/job_levels` | | Manage work types | `POST` / `PUT` / `DELETE` on `/work_types` | | Manage all review cycles | `GET /perform/review_cycles`. Grants every cycle in the company — this one isn't narrowed by the role's population. | ### Per-person data [#per-person-data] Set these on the role's **People** tab, under **What can this role see?**. They apply only to the people the role's population covers. Examples below - please see API Reference for full details. | Sub-tab | Permission | Unlocks | | :----------- | :------------------------------------------------------ | :------------------------------------------------------------------------------ | | Personal | Each profile field and table, set to None / View / Edit | Whether that field appears in `/people` responses, and whether you can write it | | Job | Hire date | `hired_on` on a person | | Job | Termination details | The `termination_*` fields, and `GET /people/{id}/lifecycles` | | Compensation | Compensation (View / Edit) | `GET` and write access to `/people/{id}/compensation/salaries` | | Perform | See objectives | `GET /perform/objectives` | | Perform | See reviews | `GET /perform/review_responses` | | Pulse | See lifecycle survey responses | `GET /pulse/lifecycle_surveys` and `/pulse/lifecycle_survey_responses` | | Pulse | See engagement survey responses | `GET /pulse/engagement_surveys` and `/pulse/engagement_survey_responses` | The Perform and Pulse sub-tabs only appear if your account has those products. **Edit implies View.** Granting Edit on a field or on Compensation also grants read access — you don't need to set both. ## Using the key [#using-the-key] Pass the key in a request header named **`X-API-KEY`**: ```bash curl https://app.peopleforce.io/api/v4/people \ -H "X-API-KEY: " ``` All API requests must be made over HTTPS — calls over plain HTTP will fail, and so will requests without authentication. Passing the API key in the X-API-KEY header ## What to expect in a response [#what-to-expect-in-a-response] Two behaviours surprise people moving from v1–v3, and both are deliberate: **People outside the role's population are absent, not refused.** Listing endpoints return only the people the key's roles cover, so a narrowly scoped key gets a short list — or an empty one — with a `200`, never a `403`. A request for a specific person outside the population returns `404`, because as far as that key is concerned the record doesn't exist. **Fields the role doesn't grant are omitted from the JSON, not returned as `null`.** If the role doesn't grant *Compensation* or a personal field, the key is simply not present in the object. Write your integration to treat a missing key as "not permitted" rather than "empty", and don't infer that a person has no value for a field you can't see. ```json { "data": { "id": 1234, "status": "active", "full_name": "Dana Whitfield", "email": "dana@example.com", "department": { "id": 12, "name": "Marketing" } } } ``` Above, `date_of_birth` and `hired_on` are absent because the role doesn't grant them — not because they're unset. The Pulse endpoints are the exception to the first rule: they return `403` when the key's roles grant no survey access at all, rather than an empty list. ## Errors [#errors] v4 returns errors as `application/problem+json` with a `status` and a `message`, and a `detail` array when there is field-level information: ```json { "status": "unauthorized", "message": "Invalid credentials provided." } ``` | Status | `message` | Usual cause | | :----- | :------------------------------------------------ | :----------------------------------------------------------------------------------------- | | `401` | Invalid credentials provided. | Missing `X-API-KEY`, a mistyped key, a key that isn't a service account, or a disabled key | | `403` | You are not authorized to access this resource. | The key's roles don't grant this action | | `404` | The requested resource could not be found. | The record doesn't exist — or is outside the role's population | | `400` | The request is invalid or cannot be processed. | Malformed or missing parameters, or a body that isn't valid JSON | | `405` | The HTTP method is not allowed for this resource. | The path exists but not with this verb; `Allow` lists the verbs it accepts | | `422` | One or more validation errors occurred. | The payload failed validation; `detail` lists each error | | `500` | An unexpected server error occurred. | A problem on our side — try again, or [contact support](mailto:support@peopleforce.io) | ## Disabling and revoking a key [#disabling-and-revoking-a-key] From **Settings → API keys** you can **disable** a key with the toggle on its row, stopping it from working without deleting it — useful while pausing an integration. A disabled key gets `401` on every request. Disabling a service account key from the API keys list To remove a key permanently, open the **…** menu on its row and choose **Delete**. Deleting a service account key from the API keys list Deleting an API key is permanent and immediate. Any integration using that key stops working at once, and the key cannot be recreated. Account for every integration before deleting. Deleting a key leaves its roles in place, so a replacement key can be attached to the same role. Deleting the *role* instead revokes everything the key could reach while leaving the credential valid — it will authenticate and return nothing. ## Troubleshooting [#troubleshooting] ### 401 on every request [#401-on-every-request] Check, in order: that the key was copied in full; that it's a **Service account** key and not a Company key; that it's still enabled in **Settings → API keys**; and that you're calling `/api/v4/…`. ### Authentication works, but every list is empty [#authentication-works-but-every-list-is-empty] The key has no role attached, or the role's population matches nobody. Open the role and confirm the key is selected under **Who is assigned this role?**, then check the rules under **Whose data can members access?**. ### A person is in the list, but a field is missing [#a-person-is-in-the-list-but-a-field-is-missing] The role doesn't grant that field for that person. Grant it on the role's **People → What can this role see?** tab — under **Personal** for profile fields and tables, **Job** for hire date and termination details, or **Compensation** for salary data. ### 403 on a write, but reads work [#403-on-a-write-but-reads-work] Writes to the org-structure lists need the matching **Manage …** permission on the role's **Company** tab. Writes to a person's data need **Edit** rather than **View** on the relevant field. # FAQ (/company/v4/getting-started/faq) ## Is using the API free? [#is-using-the-api-free] Yes — the API is free for all PeopleForce clients, and we encourage everyone to use it. ## What are the API rate limits? [#what-are-the-api-rate-limits] The current limit is **300 requests per minute**. See [Rate limits](../api-basics/rate-limit.mdx) for details. ## I don't see Webhooks, API, or Settings in PeopleForce [#i-dont-see-webhooks-api-or-settings-in-peopleforce] These pages are only visible to admin users, or users with the webhook or API permission. Contact your PeopleForce administrator to request access. ## I found a bug / I got a 500 error [#i-found-a-bug--i-got-a-500-error] Sorry to hear it — we aim for the best quality in our product. If you hit an issue, [let us know](mailto:support@peopleforce.io) and we'll work on it. A `500` response automatically notifies our team, but you're still welcome to write in and describe what happened — it helps us resolve it faster, and we'll let you know once there's a fix. ## I couldn't find the endpoint or data I was looking for [#i-couldnt-find-the-endpoint-or-data-i-was-looking-for] We cover a wide range of cases across the API and webhooks, but maybe yours is new to us. [Let us know](mailto:support@peopleforce.io) and we'll do our best to help — see also [Request a feature](./request-a-feature.mdx). # Introduction (/company/v4/getting-started) You can use the PeopleForce Company API to access your PeopleForce data — retrieving information about the entities stored in your account and performing actions on them. The API is organised around REST: predictable resource-oriented URLs, JSON request and response bodies, and standard HTTP verbs and status codes. For example, you can sync employees into PeopleForce from an external system, keep your lists of departments, divisions, and positions up to date, read time-off balances, or pull engagement-survey results. You'll need an API key first; see [Authentication](./authentication.mdx) to generate one. Then browse the full endpoint list under **API reference** in the sidebar — every operation has a live "Send" panel. ## Explore the platform's capabilities [#explore-the-platforms-capabilities] * **[Authentication](./authentication.mdx)** — generate an API key and authenticate every request. * **[Webhooks](../webhooks/index.mdx)** — subscribe to events and get notified the moment data changes in PeopleForce. Use the version switcher at the top of the sidebar to move between API versions. This page applies to every version. If you have any issues or questions, [contact us](mailto:support@peopleforce.io). # Request a feature (/company/v4/getting-started/request-a-feature) Didn't find what you were looking for in the API or webhooks? Let us know. We keep the API broad and already cover the common use cases — but if something is missing, send us a feature request and we'll see whether it can be added. [Open the requests board →](https://feedback.peopleforce.io/b/for-devs) # Starting with webhooks (/company/v4/getting-started/starting-with-webhooks) Webhooks let external services be notified when certain events happen in PeopleForce. When a subscribed event occurs, PeopleForce sends a `POST` request to each URL you configure. ## Setting up a webhook [#setting-up-a-webhook] Go to **Settings → Webhooks → Add new webhook** and fill in four fields: * **Name** — a label so you can find the webhook later. * **Payload URL** — the server that receives the webhook `POST` requests. It must use a valid SSL certificate from a publicly trusted CA. * **Secret** — an optional string used to sign requests (see below). * **Topics** — the events you want to be notified about; you can choose multiple. Save the webhook and it's live. See the full list of events in the [Webhooks overview](../webhooks/index.mdx). Creating a new webhook in Settings ## Secrets [#secrets] A secret is a shared string used to authenticate webhook deliveries. If you set one, PeopleForce signs each request and adds an `X-PeopleForce-Signature` header. Without the secret, no one else can forge a request with a matching signature. To verify the signature: * It is computed with **HMAC-SHA256** over the raw request body, keyed with your secret. * The result is a hexadecimal digest, prefixed with `sha256=`. * Compare it against the `X-PeopleForce-Signature` header using a **constant-time** comparison to avoid timing attacks. * Treat the payload as UTF-8 encoded text. ```ruby title="Ruby" def verify_signature(payload_body, signature_header) return halt 403, "Missing signature header!" if signature_header.nil? secret_key = ENV.fetch('WEBHOOK_SECRET') computed_signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret_key, payload_body) unless Rack::Utils.secure_compare(computed_signature, signature_header) return halt 403, "Signatures didn't match!" end end ``` ```python title="Python" import hmac import hashlib import os def verify_signature(payload_body, signature_header): secret_token = os.getenv('WEBHOOK_SECRET') if not signature_header: raise HTTPException(status_code=403, detail="Missing signature header!") hash_object = hmac.new(key=secret_token.encode('utf-8'), msg=payload_body.encode('utf-8'), digestmod=hashlib.sha256) expected_signature = 'sha256=' + hash_object.hexdigest() if not hmac.compare_digest(expected_signature, signature_header): raise HTTPException(status_code=403, detail="Signatures did not match!") ``` ## Testing a webhook [#testing-a-webhook] When an event fires, PeopleForce delivers the JSON payload as the body of the `POST` request. To try it out, point the Payload URL at a request inspector such as [webhook.site](https://webhook.site/) (external link — be mindful of your data), then trigger the event. For a new employee, the payload looks like: ```json { "action": "employee_create", "data": { "id": 130333, "attributes": { "employee_number": "PF124593", "hired_on": "2022-09-12", "probation_ends_on": "2022-12-12", "first_name": "John", "last_name": "Doe", "email": "john@peopleforce.io", "personal_email": null, "gender": "male", "mobile_number": "", "work_phone_number": "", "date_of_birth": "1978-08-31", "termination_effective_date": null, "termination_comment": null, "avatar_url": null }, "reporting_to": { "id": 5844, "full_name": "Ross Kate", "email": "kate@peopleforce.io" }, "employment_type": { "id": 1899, "name": "Full-Time" }, "position": { "id": 11943, "name": "Senior Developer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": null, "custom_fields": { "2692d87e-c388-49ea-b903-616bc1557746": { "name": "T-shirt size", "value": "M", "group": "Personal" } }, "meta": { "created_at": "2022-09-05T18:33:55.130+03:00", "updated_at": "2022-09-05T18:33:55.526+03:00" } } } ``` Webhook payloads are emitted by the PeopleForce platform and use the platform's own field names — they are independent of the REST API version you call. ## Execution history [#execution-history] To confirm a delivery, go to **Settings → Webhooks** and click a webhook's name to see its execution history. Click any delivery to view the raw data that was sent. Webhook execution history ## Troubleshooting [#troubleshooting] If a delivery didn't arrive or didn't look right: 1. Check the Payload URL is correct and the receiving server supports webhooks. 2. Check the webhook's run history. If it was delivered, re-check step 1, then step 3. 3. Make sure your receiving server is up and healthy. | Status | Meaning | | :---------------------------- | :------------------------------------------------------------------------------------------- | | **200 Success** | Webhook was delivered. | | **404 Not Found** | The resource could not be found — check it refers to an existing object. | | **500 Internal Server Error** | A problem on our side — try again later or [contact support](mailto:support@peopleforce.io). | If none of these help, [contact us](mailto:support@peopleforce.io). # Adding a new hire from another ATS (/company/v4/guides/ats-new-hire) When you hire someone in an external ATS and want them in PeopleForce without extra manual work, create them through the Company API. **Prerequisite:** an API key. If you don't have one, start with [Authentication](/company/v4/getting-started/authentication). ## Creating a person via the API [#creating-a-person-via-the-api] A person record is the account an employee uses to log in. It stores the core information about them — name, date of birth, email, phone number, hire date, and more. Create a person with `POST /api/v4/people`: ```bash curl -X POST https://app.peopleforce.io/api/v4/people \ -H "X-API-KEY: " \ -H "Content-Type: application/json" \ -d '{ "first_name": "Andrew", "last_name": "Doe", "email": "andrew@example.com", "hired_on": "2026-07-01" }' ``` See [Create a person](/company/v4/reference) in the Company API reference for the full field list. The accepted personal attributes are: `first_name` (required), `last_name` (required), `middle_name`, `person_number`, `email` (work email), `personal_email`, `mobile_number`, `work_phone_number`, `date_of_birth`, `hired_on`, and `gender` (passed by name). ### Custom fields [#custom-fields] Set custom fields by passing each one as a **top-level key equal to its `internal_name`** — there's no separate wrapper object: ```json { "first_name": "Andrew", "last_name": "Doe", "tax_number": "123123" } ``` A field's `internal_name` is generated from its label and can be looked up with `GET /api/v4/person_fields`. For select fields, pass the option value as a string; for multi-selects, pass an array of values. **Org placement isn't part of create.** Department, division, job title, manager, location, and employment type are kept as separate, effective-dated records (so promotions and moves preserve history) rather than fields on the person. Those sub-resources are part of the v4 roadmap but are **not yet available in the live API** — check the [API reference](/company/v4/reference) for what's currently mounted, and set org placement in the PeopleForce UI in the meantime. ## Troubleshooting [#troubleshooting] **Validation errors (422).** Check the response message — you may be missing a required field or sending invalid/duplicate data. Personal and work emails and the personal phone number **must be unique**; first and last name are required. If your company legitimately has non-unique emails or phone numbers, [contact us](mailto:support@peopleforce.io). If the person already exists, you'll get a `422`. Look them up by email with [List people](/company/v4/reference) (`GET /api/v4/people`), then update them by ID with `PUT /api/v4/people/{id}`. If your ATS can't call an API directly, check whether it integrates with [Zapier](https://zapier.com/apps/peopleforce/integrations) — Zapier can sit in between, receive your new hire, and call the PeopleForce API for you. # ERP employee directory integration (/company/v4/guides/erp-directory) This guide covers the endpoints useful for pushing data into PeopleForce from an external ERP or similar system: how to set up your structural lists, create people records, and keep them up to date. It's a starting point, not exhaustive — for anything not covered, see the [API reference](/company/v4/reference). **Prerequisite:** an API key. If you don't have one, start with [Authentication](/company/v4/getting-started/authentication). **Coverage note.** v4 is under active development and exposes a curated subset of endpoints today. Several resources the original guide used (standalone **locations**, employee **position-history** and **employment-status** records, salary **create**, and **leave**) exist in the v4 roadmap but are **not yet mounted in the live API**. Where v4 has a live equivalent it's used below; the rest are flagged as not-yet-available. Always check the [API reference](/company/v4/reference) for the currently available endpoints. **Renamed, not removed.** v4 `job_titles` is the **same catalog** as the old `positions`, and v4 `work_types` is the same as the old `employment_types` — identical records and IDs, just renamed endpoints. (Note the name reuse: `/people/:id/positions` is a *different*, employee-scoped concept — a person's dated position-history record — and is not yet mounted.) ## Step 1. Prepare your structural lists [#step-1-prepare-your-structural-lists] Before creating people, set up the org data their profiles reference. You can create these in the PeopleForce UI or via the API. | Resource | v4 endpoint | Notes | | :---------- | :------------------------- | :-------------------------------------------------------------------------------------------------------------------------- | | Divisions | `POST /api/v4/divisions` | Your larger business units. | | Departments | `POST /api/v4/departments` | Main structural units; can be nested — create parents first, then pass the parent ID when creating children. | | Job titles | `POST /api/v4/job_titles` | Catalog of titles assigned to people. **Renamed from the old `positions`** (same records and IDs). | | Work types | `POST /api/v4/work_types` | Terms of cooperation (e.g. full-time, contractor). **Renamed from the old `employment_types`** (same records and IDs). | | Locations | — | Not yet mounted in v4. The old guide used `POST /locations`. | Already have these lists? Use the matching `GET` endpoints (e.g. `GET /api/v4/divisions`, `GET /api/v4/departments`) to fetch their IDs, which you'll need when creating people. Update items with the `PUT` endpoints (e.g. `PUT /api/v4/departments/{id}`). ## Step 2. Create people [#step-2-create-people] ### Create the person record [#create-the-person-record] Create a person with `POST /api/v4/people` — see [Adding a new hire from another ATS](/company/v4/guides/ats-new-hire) for the request shape and the note on which fields v4 accepts at create time. Fetch existing people with `GET /api/v4/people`, a single person with `GET /api/v4/people/{id}`, and update with `PUT /api/v4/people/{id}`. ### Salary [#salary] You can read and update existing salary records under `/api/v4/people/{person_id}/compensation/salaries` — `GET` to list, `GET /{id}`, `PUT /{id}` to update, and `DELETE /{id}`. **Creating** a salary isn't available on the live v4 surface yet (the mounted salaries endpoint exposes list/show/update/delete only). To preserve history, the intended model is a new record per change — but until the create endpoint is mounted, set initial salary in the PeopleForce UI. **Employment status and position-history records** (`employment_statuses` and the employee-scoped `positions` in the old API) are part of the v4 roadmap but not yet mounted. They model how v4 keeps employment-status and position history (possibly via `people/{id}/lifecycles`). ## Step 3. Leave balances [#step-3-leave-balances] Receiving leave balances from an external system is **disabled by default** for security and enabled per request — [contact us](mailto:support@peopleforce.io) to set it up. The overall flow involves creating leave types and policies and assigning policies to people; you can also subscribe to [leave-request webhooks](/company/v4/webhooks/leave-requests) to react to changes. Leave types, policies, and balances are **not in the Company API v4** surface documented here. This step is preserved from the original guide pending v4 coverage. # Using PeopleForce data in user-facing solutions (/company/v4/guides/user-facing-data) Many customers surface PeopleForce data inside user-facing solutions — for example: * Showing a list of employees * Retrieving leave requests * Showing directory data such as positions or departments * Displaying vacancies on a website When you build this, implement it carefully to avoid exposing employee data and creating a security risk. ## Showing vacancies [#showing-vacancies] For displaying vacancies on a public site, use the dedicated **Career API key** and the Careers API — see [Own career site integration](/careers/v1/guides/own-career-site). The Career key is limited to non-sensitive vacancy data, so it's safe in a browser. ## Everything else: never expose the API key [#everything-else-never-expose-the-api-key] For all other data, **never put a Service account API key in the browser (frontend)**. Protect it behind a backend proxy service. This matters even for internal, non-public solutions — a key exposed to your own team is still a data-security risk. A good pattern is a **two-tier architecture**: the frontend talks only to your backend, and your backend holds the API key and calls PeopleForce. 1. The user-facing app requests data from your backend — it never references a PeopleForce API key. 2. The backend receives the request and calls PeopleForce, using the API key it holds server-side (optionally filtering by use case). 3. PeopleForce responds to the backend. 4. The backend reduces the response to only the fields the frontend needs, then returns it. This keeps the API key encapsulated in the backend, where it's never exposed to the client. Always ensure your API key has the least permissions required for the user of the key. # Candidates and vacancies (/company/v4/webhooks/candidates-and-vacancies) For PeopleRecruit users, these webhooks cover the recruitment lifecycle: * `applicant_create` — a candidate is created * `vacancy_offer_accept` — a candidate accepted and signed an offer * `vacancy_create` — a vacancy is created * `vacancy_application_create` — a candidate applied or was added to a vacancy ## Applicant created [#applicant-created] Triggered instantly when a new applicant is created. Action: `applicant_create`. Custom fields are keyed by their field ID, each with a `name` and `value`. ```json { "action": "applicant_create", "data": { "id": 1461719, "attributes": { "full_name": "Pavlo Skrypka", "position": null, "email": "pavlo@example.com", "phone_numbers": ["380948593490"], "urls": [] }, "custom_fields": { "70f2c0d7-559a-4066-be3a-1cc53e547c52": { "name": "Driving license", "value": null }, "c162e147-7c10-4bee-b53b-72f0183edbc2": { "name": "Special notes", "value": null }, "18700c50-a0ac-40e1-8e52-f636a64a3edc": { "name": "desired_salary", "value": null } }, "meta": { "created_at": "2022-09-21T16:17:10.410+01:00", "updated_at": "2022-09-21T16:17:10.416+01:00" } } } ``` ## Applicant offer accepted [#applicant-offer-accepted] Triggered instantly when an applicant accepts and signs an offer. Action: `vacancy_offer_accept`. The payload does not include the offer document. ```json { "action": "vacancy_offer_accept", "data": { "id": 7978, "attributes": { "accepted_at": "2022-09-21T16:20:21.035+01:00", "rejected_at": null, "viewed_at": "2022-09-21T16:20:17.229+01:00" }, "applicant": { "id": 1461719, "full_name": "Pavlo Skrypka", "email": "pavlo@example.io" }, "vacancy": { "id": 526, "title": "QA Tester", "status": "accepted" }, "meta": { "created_at": "2022-09-21T16:19:58.757+01:00", "updated_at": "2022-09-21T16:20:21.013+01:00" } } } ``` ## Vacancy created [#vacancy-created] Triggered instantly when a vacancy is created. Action: `vacancy_create`. The `description` is delivered as HTML. ```json { "action": "vacancy_create", "data": { "id": 56721, "attributes": { "name": "UX Designer", "description": "
" }, "custom_fields": { "478214e1-0b81-4048-a6a3-63d3088d1ee0": { "name": "Custom field", "value": "Custom text" } }, "meta": { "created_at": "2024-02-01T16:20:40.896+00:00", "updated_at": "2024-02-01T16:20:40.906+00:00" } } } ``` ## Vacancy application created [#vacancy-application-created] Triggered instantly when a candidate applies or is added to a vacancy. Action: `vacancy_application_create`. ```json { "action": "vacancy_application_create", "data": { "id": 3102839, "candidate": { "id": 2151205, "full_name": "Jasmin Doe", "email": null }, "vacancy": { "id": 56721, "title": "UX Designer" }, "meta": { "created_at": "2024-02-01T16:27:14.814+00:00", "updated_at": "2024-02-01T16:27:14.814+00:00" } } } ``` # Employee compensation (/company/v4/webhooks/employee-compensation) Alongside an employee's profile attributes, PeopleForce keeps separate records for **position** and **salary**. To preserve the history of salary reviews and other changes, add a new record rather than editing the existing one. The salary record emits these webhooks: * `employee_salary_create` — a salary record is added * `employee_salary_update` — a salary record is updated Both fire immediately for any employee, active or terminated. ## Employee salary created [#employee-salary-created] Action: `employee_salary_create`. ```json { "action": "employee_salary_create", "data": { "id": 80093, "attributes": { "effective_on": "2022-09-21", "amount": "45000.0", "per": "month", "currency_code": "UAH", "comment": "" }, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com" }, "meta": { "created_at": "2022-09-21T15:59:57.173+01:00", "updated_at": "2022-09-21T15:59:57.173+01:00" } } } ``` ## Employee salary updated [#employee-salary-updated] Action: `employee_salary_update`. The payload shape matches `employee_salary_create`, with the updated values. # Employee job profile (/company/v4/webhooks/employee-job-profile) Alongside an employee's profile attributes, PeopleForce keeps a **job profile** history for each employee. As with position and salary, add a new record rather than editing the existing one so the history of moves is preserved. The job profile record emits these webhooks: * `employee_job_profile_create` — a job profile record is added * `employee_job_profile_update` — a job profile record is updated Both fire immediately for any employee, active or terminated. ## When they fire [#when-they-fire] A delivery is sent when the record is saved through any of these paths: * the **Job profile** section of an employee's profile * the Public API (`POST` / `PATCH` on an employee's job profiles) * an approved employee change request that adds a job profile record — this emits `employee_job_profile_create` * compliance workforce-planning verification, which adds a record `employee_job_profile_update` fires only when the save changes at least one stored value. A request that submits identical values sends nothing. A failed save sends nothing. ## Employee job profile created [#employee-job-profile-created] Action: `employee_job_profile_create`. ```json { "action": "employee_job_profile_create", "data": { "id": 4, "attributes": { "effective_on": "2026-08-14" }, "employee": { "id": 6, "employee_number": "IT-1029", "first_name": "Charles", "last_name": "Lancaster", "email": "charles@example.com" }, "job_profile": { "id": 2, "name": "Senior Developer" }, "job_group": { "id": 1, "name": "Engineering" }, "meta": { "created_at": "2026-08-14T14:18:40.723Z", "updated_at": "2026-08-14T14:18:40.723Z" } } } ``` ## Employee job profile updated [#employee-job-profile-updated] Action: `employee_job_profile_update`. The payload shape matches `employee_job_profile_create`, with the updated values. ```json { "action": "employee_job_profile_update", "data": { "id": 2, "attributes": { "effective_on": "2026-08-01" }, "employee": { "id": 4, "employee_number": null, "first_name": "Alex", "last_name": "Johnstone", "email": "alex@example.com" }, "job_profile": { "id": 1, "name": "Developer" }, "job_group": { "id": 1, "name": "Engineering" }, "meta": { "created_at": "2026-08-14T14:15:40.159Z", "updated_at": "2026-08-14T14:18:09.037Z" } } } ``` ## Field reference [#field-reference] | Field | Type | Notes | | :----------------------------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------- | | `data.id` | integer | The job profile history record ID, not the job profile ID. | | `data.attributes.effective_on` | date | The date the record takes effect. | | `data.employee` | object | Employee identity: `id`, `employee_number`, `first_name`, `last_name`, and `email`. `employee_number` is `null` when unset. | | `data.job_profile` | object | The assigned job profile: `id` and `name`. | | `data.job_group` | object or null | The job group the profile belongs to: `id` and `name`. `null` if the profile has no job group. | | `data.meta` | object | `created_at` and `updated_at` for the history record. | # Employee position (/company/v4/webhooks/employee-position) Alongside an employee's profile attributes (name, hire date, and so on), PeopleForce keeps separate records for **position** and **salary**. To preserve the history of promotions and other changes, add a new record rather than editing the existing one. The position record emits these webhooks: * `employee_position_create` — a position record is added * `employee_position_update` — a position record is updated Both fire immediately for any employee, active or terminated. ## Employee position created [#employee-position-created] Action: `employee_position_create`. ```json { "action": "employee_position_create", "data": { "id": 224222, "attributes": { "effective_on": "2022-12-01" }, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com" }, "reporting_to": { "id": 10277, "first_name": "Pahney", "last_name": "Zhelezo" }, "position": { "id": 11943, "name": "Senior Developer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": { "id": 28693, "name": "Vinnytsia" }, "meta": { "created_at": "2022-09-21T14:57:29.151+01:00", "updated_at": "2022-09-21T14:57:29.151+01:00" } } } ``` ## Employee position updated [#employee-position-updated] Action: `employee_position_update`. The payload shape matches `employee_position_create`, with the updated values. # Employee profile (/company/v4/webhooks/employee-profile) An employee profile is the account of an employee. Employees may log in using their email or Active Directory username, so take care when changing those fields. The employee profile emits these webhooks: * `employee_create` — an employee profile is created * `employee_update` — an employee profile is updated * `employee_start` — an employee's first day * `employee_terminate` — an employee is terminated * `employee_termination_revert` — a termination is reverted (reactivation) Webhook payloads are emitted by the PeopleForce platform and use the platform's own field names (e.g. `position`, `employee_number`) — independent of the Company API v4 resource naming. ## Employee profile created [#employee-profile-created] Triggered instantly when a new employee profile is created. Action: `employee_create`. ```json { "action": "employee_create", "data": { "id": 133710, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-09-26", "probation_ends_on": "2022-12-26", "first_name": "Volodymyr", "last_name": "Markovich", "email": "mv@example.com", "personal_email": null, "gender": "male", "mobile_number": "", "work_phone_number": "", "date_of_birth": "1978-08-29", "termination_effective_date": null, "termination_comment": null, "avatar_url": null }, "reporting_to": { "id": 10277, "full_name": "Zhelezo Pahney", "email": "zp@peopleforce.io" }, "employment_type": { "id": 1899, "name": "Full-Time" }, "position": { "id": 114651, "name": "IT Support Engineer" }, "department": { "id": 5094, "name": "IT" }, "division": { "id": 1819, "name": "Europe" }, "location": { "id": 28693, "name": "Vinnytsia" }, "custom_fields": { "a25b06fb-bcfb-4aac-b695-6aff138fac36": { "name": "HR manager", "value": "5844", "group": "Personal" } }, "meta": { "created_at": "2022-09-21T16:29:07.894+03:00", "updated_at": "2022-09-21T16:29:08.713+03:00" } } } ``` ## Employee profile updated [#employee-profile-updated] Triggered instantly when an existing employee profile is updated. Action: `employee_update`. The payload shape matches `employee_create`, with the updated field values. ## Employee first day [#employee-first-day] Triggered in two cases: 1. Instantly, if an employee is created with a hire date of today. 2. Every night at 01:00 (UTC) for all employees whose hire date is that day. Action: `employee_start`. The payload shape matches `employee_create`. ```json { "action": "employee_start", "data": { "id": 133710, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-09-12", "first_name": "John", "last_name": "Doe", "email": "john@peopleforce.io" }, "meta": { "created_at": "2022-09-05T15:33:55.130Z", "updated_at": "2022-09-05T15:33:55.526Z" } } } ``` ## Employee terminated [#employee-terminated] Triggered in two cases: 1. Instantly, if an employee is terminated with a past date. 2. Every night at 01:00 (UTC) for all employees whose termination date is that day. Action: `employee_terminate`. The payload shape matches `employee_create`, with `termination_effective_date` set and most relational fields `null`. ```json { "action": "employee_terminate", "data": { "id": 123512, "attributes": { "employee_number": "IT-1029", "hired_on": "2022-07-26", "first_name": "John", "last_name": "Doe", "termination_effective_date": "2022-11-07", "termination_comment": "" }, "reporting_to": null, "position": null, "department": null, "division": null, "location": null, "meta": { "created_at": "2022-07-29T10:42:43.354+01:00", "updated_at": "2022-11-10T11:12:32.102+00:00" } } } ``` ## Termination reverted [#termination-reverted] Triggered when an employee's termination is reverted — either cancelled before the termination date, or a terminated employee is reactivated. Action: `employee_termination_revert`. The payload shape matches `employee_create`. There is no `employee_activate` webhook. Reactivation and un-termination are delivered as `employee_termination_revert`. # Employee custom table rows (/company/v4/webhooks/employee-table-rows) **Custom tables** are the repeating tables you define yourself on an employee's profile — certifications, equipment, dependants, and so on. Each row emits its own event, so an integration can stay in step without polling. The custom table row emits these webhooks: * `employee_table_row_create` — a row is added * `employee_table_row_update` — a row is changed * `employee_table_row_destroy` — a row is deleted Subscribe to each one independently. Selecting one does not subscribe you to the other two, and existing endpoints gain none of them until you add them. Every payload is self-contained: it carries the employee, the table, and every column with its current value, so you do not need a follow-up API call. Values are the current state only — there is no before-and-after comparison. ## When they fire [#when-they-fire] A delivery is sent when the row is saved or deleted through any of these paths: * the employee's profile * the Public API (`POST` / `PATCH` / `DELETE` on an employee's table rows) * an approved employee change request that adds a row — this emits `employee_table_row_create` One event is sent per changed row, so a change request touching three rows sends three deliveries. ### When they do not fire [#when-they-do-not-fire] These topics cover **custom** tables only. Rows in PeopleForce system tables — position, salary, employment history, and compliance tables — never emit them. Nothing is sent when: * the save changes no stored value, so an identical update is silent * the create, update, delete, or approval fails * a change request is submitted, rejected, or cancelled — only approval emits * a row is created by a **hire form or preboarding form** ## Employee custom table row created [#employee-custom-table-row-created] Action: `employee_table_row_create`. ```json { "action": "employee_table_row_create", "data": { "id": 88213, "employee": { "id": 133710, "employee_number": "IT-1029", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com" }, "table": { "id": 4471, "name": "Certifications", "internal_name": "certifications" }, "columns": [ { "id": 90114, "name": "Level", "internal_name": "level", "type": "EmployeeTableColumns::Text", "value": "Professional" }, { "id": 90115, "name": "Certified on", "internal_name": "certified_on", "type": "EmployeeTableColumns::Date", "value": null }, { "id": 90116, "name": "Score", "internal_name": "score", "type": "EmployeeTableColumns::Number", "value": "87" }, { "id": 90117, "name": "Renewable", "internal_name": "renewable", "type": "EmployeeTableColumns::CheckBox", "value": "1" }, { "id": 90118, "name": "Rating", "internal_name": "rating", "type": "EmployeeTableColumns::SingleSelect", "value": { "id": "3312", "name": "Excellent" } }, { "id": 90119, "name": "Topics", "internal_name": "topics", "type": "EmployeeTableColumns::MultipleSelect", "value": [ { "id": "3315", "name": "Ruby" }, { "id": "3316", "name": "Rails" } ] }, { "id": 90120, "name": "Mentor", "internal_name": "mentor", "type": "EmployeeTableColumns::Reference", "value": { "id": "133711", "name": "Grace Hopper" } }, { "id": 90121, "name": "Notes", "internal_name": "notes", "type": "EmployeeTableColumns::LongText", "value": "Renewal due next year" } ], "meta": { "created_at": "2026-08-17T09:41:12.204Z", "updated_at": "2026-08-17T09:41:12.204Z" } } } ``` ## Employee custom table row updated [#employee-custom-table-row-updated] Action: `employee_table_row_update`. The payload shape matches `employee_table_row_create`, with the row's values after the change. ## Employee custom table row deleted [#employee-custom-table-row-deleted] Action: `employee_table_row_destroy`. The payload shape matches `employee_table_row_create`, and carries the row's final values as they stood before deletion. ## Field reference [#field-reference] | Field | Type | Notes | | :----------------------------- | :------ | :------------------------------------------------------------------------------------------ | | `data.id` | integer | The row ID. | | `data.employee` | object | Employee identity: `id`, `employee_number`, `first_name`, `last_name`, and `email`. | | `data.table` | object | The custom table: `id`, `name`, and `internal_name`. | | `data.columns` | array | Every column on the table, in the order shown in the product. Deleted columns are excluded. | | `data.columns[].id` | integer | The column ID. | | `data.columns[].internal_name` | string | The stable API name. Prefer it over `name`, which admins can rename freely. | | `data.columns[].type` | string | The column type — see the table below. | | `data.columns[].value` | varies | The current value — see the table below. | | `data.meta` | object | `created_at` and `updated_at` for the row. | ### Column values by type [#column-values-by-type] Every column on the table appears in `columns`, whether or not it holds a value. An empty column is present with a `null` value. | `type` | `value` shape | Empty | | :------------------------------------- | :------------------------------------------ | :----- | | `EmployeeTableColumns::Text` | string | `null` | | `EmployeeTableColumns::Number` | string, e.g. `"87"` | `null` | | `EmployeeTableColumns::Date` | string, e.g. `"2026-08-14"` | `null` | | `EmployeeTableColumns::CheckBox` | string, `"1"` when ticked or `"0"` when not | `null` | | `EmployeeTableColumns::LongText` | string, HTML stripped to plain text | `null` | | `EmployeeTableColumns::SingleSelect` | `{ "id": "…", "name": "…" }` | `null` | | `EmployeeTableColumns::MultipleSelect` | array of `{ "id": "…", "name": "…" }` | `[]` | | `EmployeeTableColumns::Reference` | `{ "id": "…", "name": "…" }` — an employee | `null` | Four details to code against: * **Scalars are JSON strings, not JSON numbers or booleans.** A number column sends `"87"`, a date sends `"2026-08-14"`, and a checkbox sends `"1"` or `"0"`. Both write paths store the value as text and the payload returns it uncast, so parse rather than assume. Rows loaded by an older import may still hold a raw JSON number. * An **unticked** checkbox sends `"0"`, which is a real value, not an empty one. Only a checkbox that was never set sends `null`. * A multi-select is **always an array**, never `null`. An empty one is `[]`. * The `id` inside a select, multi-select, or reference value is a **string**, because that is how the row stores it. The surrounding `columns[].id` and `data.id` are integers. A reference value keeps its stored `id` even when the referenced employee can no longer be found, in which case `name` is `null`. ## Testing a payload [#testing-a-payload] The sample-payload endpoint (`GET /webhooks?key=`) does not yet cover the three custom table row topics and returns `204 No Content` for them. To see a real payload, subscribe an endpoint, change a row, then open the delivery in **Settings → Webhooks → delivery history**. # Webhooks overview (/company/v4/webhooks) Webhooks let you subscribe to events happening in PeopleForce. Rather than polling the API, you configure an endpoint and PeopleForce sends it an HTTP `POST` request whenever a subscribed event fires. You manage subscriptions — and review past deliveries and their payloads — in your PeopleForce **Settings**. See [Starting with webhooks](../getting-started/starting-with-webhooks.mdx) for how to create one, secure it with a signing secret, and test deliveries. Every delivery has the shape `{ "action": "", "data": { … } }`, where `action` is the topic name (e.g. `employee_create`). Doesn't your integration support receiving webhooks? Check whether it supports Zapier — PeopleForce offers a range of [triggers and actions for Zapier](https://zapier.com/apps/peopleforce/integrations). ## Available topics [#available-topics] PeopleForce offers the webhook topics below. Availability depends on the PeopleForce modules enabled for your account. ### Employee [#employee] | Topic | Description | | :---------------------------------------- | :---------------------------------------------------- | | `employee_create` | An employee is created. | | `employee_update` | An employee is updated. | | `employee_start` | An employee's first day (hire date is reached). | | `employee_terminate` | An employee is terminated. | | `employee_termination_revert` | An employee's termination is reverted (reactivation). | | `employee_position_create` | An employee position record is created. | | `employee_position_update` | An employee position record is updated. | | `employee_job_profile_create` | An employee job profile record is created. | | `employee_job_profile_update` | An employee job profile record is updated. | | `employee_table_row_create` | A row in an employee's custom table is created. | | `employee_table_row_update` | A row in an employee's custom table is updated. | | `employee_table_row_destroy` | A row in an employee's custom table is deleted. | | `employee_salary_create` | An employee salary record is created. | | `employee_salary_update` | An employee salary record is updated. | | `employee_additional_compensation_create` | An additional compensation is created. | | `employee_additional_compensation_update` | An additional compensation is updated. | | `employee_employment_status_create` | An employment status record is created. | | `employee_employment_status_update` | An employment status record is updated. | | `external_user_create` | An external user is created. | ### Leave [#leave] | Topic | Description | | :----------------------- | :---------------------------- | | `leave_request_create` | A leave request is created. | | `leave_request_approve` | A leave request is approved. | | `leave_request_reject` | A leave request is rejected. | | `leave_request_withdraw` | A leave request is withdrawn. | | `leave_request_destroy` | A leave request is deleted. | ### Recruitment [#recruitment] | Topic | Description | | :----------------------------- | :--------------------------------------------------------------------- | | `applicant_create` | A candidate is created. | | `applicant_destroy` | A candidate is deleted. | | `vacancy_create` | A vacancy is created. | | `vacancy_application_create` | A candidate applied or was added to a vacancy. | | `vacancy_application_movement` | An application moved pipeline stage, was disqualified, or requalified. | | `vacancy_offer_accept` | A vacancy offer was accepted by the candidate. | | `vacancy_offer_reject` | A vacancy offer was rejected by the candidate. | ### Time [#time] | Topic | Description | | :------------------------- | :------------------------------- | | `overtime_request_create` | An overtime request is created. | | `overtime_request_update` | An overtime request is updated. | | `overtime_request_destroy` | An overtime request is deleted. | | `overtime_request_approve` | An overtime request is approved. | ### Other [#other] | Topic | Description | | :------------- | :------------------------------------------------------ | | `survey_start` | A survey is launched (moves from Scheduled to Running). | A **workflow** webhook can also be configured as an *action* inside a workflow — it isn't part of the subscribable topic list above. See [Other webhooks](./other-webhooks.mdx). ## Delivery headers [#delivery-headers] Every delivery includes: * `Content-Type: application/json` * `X-PeopleForce-Endpoint` — the webhook endpoint ID * `X-PeopleForce-Delivery` — a unique delivery ID * `X-PeopleForce-Signature` — present only when the endpoint has a secret configured (see [signing](../getting-started/starting-with-webhooks.mdx#secrets)) Deliveries time out after 5 seconds and require a valid TLS certificate. The pages in this section group these topics and show example payloads: [Employee profile](./employee-profile.mdx), [Employee position](./employee-position.mdx), [Employee job profile](./employee-job-profile.mdx), [Employee custom table rows](./employee-table-rows.mdx), [Employee compensation](./employee-compensation.mdx), [Leave requests](./leave-requests.mdx), [Candidates and vacancies](./candidates-and-vacancies.mdx), [Overtime requests](./overtime-requests.mdx), and [Other webhooks](./other-webhooks.mdx). # Leave requests (/company/v4/webhooks/leave-requests) PeopleForce offers five webhook topics covering the leave-request lifecycle: * `leave_request_create` — a leave request is created * `leave_request_approve` — a leave request is approved * `leave_request_reject` — a leave request is rejected * `leave_request_withdraw` — a leave request is withdrawn * `leave_request_destroy` — a leave request is deleted If the leave policy has file attachments enabled or required, the attached files are not included in the webhook payload. ## Leave request created [#leave-request-created] Triggered instantly, as soon as the leave request is created. Action: `leave_request_create`. ```json { "action": "leave_request_create", "data": { "id": 1062480, "attributes": { "employee_id": 2632, "starts_on": "2024-01-31", "ends_on": "2024-02-01", "amount": "2.0", "description": "Vacation with family", "unit": "days", "state": "approved" }, "entries": [ { "occurs_on": "2024-01-31", "amount": "1.0" }, { "occurs_on": "2024-02-01", "amount": "1.0" } ], "leave_type": { "id": 13289, "name": "Vacation" }, "meta": { "created_at": "2024-02-01T15:43:50.841+00:00", "updated_at": "2024-02-01T15:43:50.878+00:00" } } } ``` ## Leave request approved [#leave-request-approved] Triggered instantly, as soon as the last approver in the approval flow approves the request and its status changes to Approved. Action: `leave_request_approve`. ```json { "action": "leave_request_approve", "data": { "id": 345842, "attributes": { "employee_id": 2632, "starts_on": "2022-09-20", "ends_on": "2022-09-23", "amount": "32.0", "description": "", "unit": "hours", "state": "approved" }, "entries": [ { "occurs_on": "2022-09-20", "amount": "8.0" }, { "occurs_on": "2022-09-21", "amount": "8.0" }, { "occurs_on": "2022-09-22", "amount": "8.0" }, { "occurs_on": "2022-09-23", "amount": "8.0" } ], "leave_type": { "id": 4098, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:08:31.562+01:00", "updated_at": "2022-09-21T16:08:36.540+01:00" } } } ``` ## Leave request rejected [#leave-request-rejected] Triggered instantly, as soon as at least one approver in the approval flow rejects the request and its status changes to Rejected. Action: `leave_request_reject`. ```json { "action": "leave_request_reject", "data": { "id": 345847, "attributes": { "employee_id": 2632, "starts_on": "2022-09-06", "ends_on": "2022-09-11", "amount": "32.0", "description": "", "unit": "hours", "state": "rejected" }, "entries": [ { "occurs_on": "2022-09-06", "amount": "8.0" }, { "occurs_on": "2022-09-07", "amount": "8.0" }, { "occurs_on": "2022-09-08", "amount": "8.0" }, { "occurs_on": "2022-09-09", "amount": "8.0" }, { "occurs_on": "2022-09-10", "amount": "0.0" }, { "occurs_on": "2022-09-11", "amount": "0.0" } ], "leave_type": { "id": 4098, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:10:25.052+01:00", "updated_at": "2022-09-21T16:10:28.284+01:00" } } } ``` ## Leave request withdrawn [#leave-request-withdrawn] Triggered instantly, as soon as the leave request is withdrawn and its status changes to Withdrawn. Action: `leave_request_withdraw`. ```json { "action": "leave_request_withdraw", "data": { "id": 345850, "attributes": { "employee_id": 2632, "starts_on": "2022-09-28", "ends_on": "2022-09-30", "amount": "3.0", "description": "", "unit": "days", "state": "withdrawn" }, "entries": [ { "occurs_on": "2022-09-28", "amount": "1.0" }, { "occurs_on": "2022-09-29", "amount": "1.0" }, { "occurs_on": "2022-09-30", "amount": "1.0" } ], "leave_type": { "id": 3916, "name": "Day off" }, "meta": { "created_at": "2022-09-21T16:12:48.034+01:00", "updated_at": "2022-09-21T16:13:13.260+01:00" } } } ``` # Other webhooks (/company/v4/webhooks/other-webhooks) ## Workflow triggered [#workflow-triggered] This webhook can only be configured as an action inside a workflow. The payload carries the full employee record the workflow acts on. ```json { "data": { "id": 6369, "attributes": { "employee_number": null, "hired_on": "2017-08-21", "first_name": "Scott", "middle_name": "", "last_name": "Pilgrim", "email": "scott@example.com", "termination_effective_date": null, "termination_reason": null, "termination_type": null }, "reporting_to": null, "position": null, "department": { "id": 105328, "name": "Corporate" }, "division": null, "location": { "id": 3134, "name": "Spain" }, "custom_fields": { "a25b06fb-bcfb-4aac-b695-6aff138fac36": { "name": "HR manager", "value": null, "group": "Personal" } }, "meta": { "created_at": "2020-02-03T12:08:03.934+01:00", "updated_at": "2025-10-20T15:42:42.928+02:00" } } } ``` ## Survey launched [#survey-launched] Triggered instantly when a survey moves from "Scheduled" to "Running". Action: `survey_start`. ```json { "action": "survey_start", "data": { "id": 10441, "attributes": { "name": "Stress management", "state": "running", "locale": "en", "starts_at": "2024-02-01T00:00:00.000Z", "ends_at": "2024-02-29T00:00:00.000Z" }, "meta": { "created_at": "2024-02-01T16:32:17.956Z", "updated_at": "2024-02-01T16:34:00.563Z" } } } ``` # Overtime requests (/company/v4/webhooks/overtime-requests) PeopleForce offers four webhook topics covering the overtime-request lifecycle: * `overtime_request_create` — an overtime request is created * `overtime_request_update` — an overtime request is updated * `overtime_request_approve` — an overtime request is approved * `overtime_request_destroy` — an overtime request is deleted ## Overtime request created [#overtime-request-created] Triggered instantly, as soon as the overtime request is created. Action: `overtime_request_create`. ```json { "action": "overtime_request_create", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T20:30:00.000+00:00", "minutes": 150, "comment": "Release deployment support", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:12:03.841+00:00" } } } ``` ## Overtime request updated [#overtime-request-updated] Triggered instantly, as soon as the overtime request is updated. Action: `overtime_request_update`. ```json { "action": "overtime_request_update", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:20:47.116+00:00" } } } ``` ## Overtime request approved [#overtime-request-approved] Triggered instantly, as soon as the last approver in the approval flow approves the request and its status changes to Approved. Action: `overtime_request_approve`. ```json { "action": "overtime_request_approve", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "approved" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T10:05:12.298+00:00" } } } ``` ## Overtime request deleted [#overtime-request-deleted] Triggered instantly, as soon as the overtime request is deleted. Action: `overtime_request_destroy`. ```json { "action": "overtime_request_destroy", "data": { "id": 4821, "attributes": { "date": "2026-08-14", "starts_at": "2026-08-14T18:00:00.000+00:00", "ends_at": "2026-08-14T21:00:00.000+00:00", "minutes": 180, "comment": "Release deployment support, extended", "state": "pending" }, "project": { "id": 132, "name": "Platform migration" }, "employee": { "id": 2632, "first_name": "Scott", "last_name": "Pilgrim" }, "meta": { "created_at": "2026-08-13T09:12:03.841+00:00", "updated_at": "2026-08-13T09:12:03.841+00:00" } } } ``` # Create an asset assignment (/company/v1/reference/assets/post-asset_assignments) # Delete an asset assignment (/company/v1/reference/assets/delete-asset_assignments-id) # Update an asset assignment (/company/v1/reference/assets/put-asset_assignments-id) # List all asset categories (/company/v1/reference/assets/get-asset_categories) # Create an asset category (/company/v1/reference/assets/post-asset_categories) # Delete an asset category (/company/v1/reference/assets/delete-asset_categories-id) # Update an asset category (/company/v1/reference/assets/put-asset_categories-id) # List all assets (/company/v1/reference/assets/get-assets) # Create an asset (/company/v1/reference/assets/post-assets) # Get an asset (/company/v1/reference/assets/get-assets-id) # Delete an asset (/company/v1/reference/assets/delete-assets-id) # Update an asset (/company/v1/reference/assets/put-assets-id) # List company calendar events (/company/v1/reference/calendars/get-calendars) # List compensation types (/company/v1/reference/compensation-types/get-compensation_types) # List all departments (/company/v1/reference/departments/get-departments) # Create a department (/company/v1/reference/departments/post-departments) # Delete a department (/company/v1/reference/departments/delete-departments-id) # Update a department (/company/v1/reference/departments/put-departments-id) # List all divisions (/company/v1/reference/divisions/get-divisions) # Create a division (/company/v1/reference/divisions/post-divisions) # Delete a division (/company/v1/reference/divisions/delete-divisions-id) # Update a division (/company/v1/reference/divisions/put-divisions-id) # List document folders (/company/v1/reference/document-folders/get-document_folders) # Create a document folder (/company/v1/reference/document-folders/post-document_folders) # Get a document folder (/company/v1/reference/document-folders/get-document_folders-id) # Delete a document folder (/company/v1/reference/document-folders/delete-document_folders-id) # Update a document folder (/company/v1/reference/document-folders/put-document_folders-id) # List all employee fields (/company/v1/reference/employees/get-employee_fields) # List all employees (/company/v1/reference/employees/get-employees) # Create an employee (/company/v1/reference/employees/post-employees) # List terminated employees (/company/v1/reference/employees/get-employees-terminated) # List employee additional compensations (/company/v1/reference/employees/get-employees-id-additional_compensations) # Create an employee additional compensation (/company/v1/reference/employees/post-employees-id-additional_compensations) # Delete an employee additional compensation (/company/v1/reference/employees/delete-employees-id-additional_compensations-id) # List employees certifications (/company/v1/reference/employees/get-employees-id-certifications) # List employee compensations (/company/v1/reference/employees/get-employees-employee_id-compensations) # Create an employee compensation (/company/v1/reference/employees/post-employees-employee_id-compensations) # Delete an employee compensation (/company/v1/reference/employees/delete-employees-employee_id-compensations-id) # Update an employee compensation (/company/v1/reference/employees/put-employees-employee_id-compensations-id) # List of employee documents (/company/v1/reference/employees/get-employees-id-documents) # Create an employee document (/company/v1/reference/employees/post-employees-id-documents) # Get an employee document (/company/v1/reference/employees/get-employees-id-documents-document_id) # Delete an employee document (/company/v1/reference/employees/delete-employees-employee_id-documents-id) # List of employee educations (/company/v1/reference/employees/get-employees-id-educations) # List employees employment statuses (/company/v1/reference/employees/get-employees-id-employment_statuses) # Create an employee employment status (/company/v1/reference/employees/post-employees-employee_id-employment_statuses) # Delete an employee employment status (/company/v1/reference/employees/delete-employees-employee_id-employment_statuses-id) # Update an employee employment status (/company/v1/reference/employees/put-employees-employee_id-employment_statuses-id) # List of employee leave types (/company/v1/reference/employees/get-employees-id-leave_types) # Assign a leave policy to an employee (/company/v1/reference/employees/post-employees-id-leave_types) # Delete an employee leave type (/company/v1/reference/employees/delete-employees-employee_id-leave_types-id) # List employees positions (/company/v1/reference/employees/get-employees-id-positions) # Create an employee position (/company/v1/reference/employees/post-employees-employee_id-positions) # Delete an employee position (/company/v1/reference/employees/delete-employees-employee_id-positions-id) # Update an employee position (/company/v1/reference/employees/put-employees-employee_id-positions-id) # List of employee salaries (/company/v1/reference/employees/get-employees-employee_id-salaries) # Create an employee salary (/company/v1/reference/employees/post-employees-employee_id-salaries) # Delete an employee salary (/company/v1/reference/employees/delete-employees-employee_id-salaries-id) # Update an employee salary (/company/v1/reference/employees/put-employees-employee_id-salaries-id) # Get an employee (/company/v1/reference/employees/get-employees-id) # Update an employee (/company/v1/reference/employees/put-employees-id) # Activate an employee (/company/v1/reference/employees/post-employees-id-activate) # List of employee assets (/company/v1/reference/employees/get-employees-id-assets) # List of employee dependents (/company/v1/reference/employees/get-employees-id-dependents) # List of employee emergency contacts (/company/v1/reference/employees/get-employees-id-emergency_contacts) # List of employee holidays (/company/v1/reference/employees/get-employees-id-holidays) # List of employee leave balances (/company/v1/reference/employees/get-employees-id-leave_balances) # List of employee notes (/company/v1/reference/employees/get-employees-id-notes) # List of employee tasks (/company/v1/reference/employees/get-employees-id-tasks) # Terminate an employee (/company/v1/reference/employees/post-employees-employee_id-terminate) # Update an employee leave balance (External Policy) (/company/v1/reference/employees/put-employees-id-update_leave_balance) # List all employment types (/company/v1/reference/employment-types/get-employment_types) # Create an employment type (/company/v1/reference/employment-types/post-employment_types) # Delete an employment type (/company/v1/reference/employment-types/delete-employment_types-id) # Update an employment type (/company/v1/reference/employment-types/put-employment_types-id) # List all holiday policies (/company/v1/reference/holidays/get-holiday_policies) # Create a holiday policy (/company/v1/reference/holidays/post-holiday_policies) # Get a holiday policy (/company/v1/reference/holidays/get-holiday_policies-id) # Delete a holiday policy (/company/v1/reference/holidays/delete-holiday_policies-id) # Update a holiday policy (/company/v1/reference/holidays/put-holiday_policies-id) # List all holidays (/company/v1/reference/holidays/get-holidays) # Create a holiday (/company/v1/reference/holidays/post-holidays) # Delete a holiday (/company/v1/reference/holidays/delete-holidays-id) # Update a holiday (/company/v1/reference/holidays/put-holidays-id) # List all job groups (/company/v1/reference/job-groups/get-job_groups) # Create a job group (/company/v1/reference/job-groups/post-job_groups) # Delete a job group (/company/v1/reference/job-groups/delete-job_groups-id) # Update a job group (/company/v1/reference/job-groups/put-job_groups-id) # List all job profiles (/company/v1/reference/job-profiles/get-job_profiles) # Create a job profile (/company/v1/reference/job-profiles/post-job_profiles) # Get a job profile (/company/v1/reference/job-profiles/get-job_profiles-id) # Delete a job profile (/company/v1/reference/job-profiles/delete-job_profiles-id) # Update a job profile (/company/v1/reference/job-profiles/put-job_profiles-id) # List all leave requests (/company/v1/reference/leaves/get-leave_requests) # Create a leave request (/company/v1/reference/leaves/post-leave_requests) # List pending leave requests (/company/v1/reference/leaves/get-leave_requests-pending) # Get a leave request (/company/v1/reference/leaves/get-leave_requests-id) # Delete a leave request (/company/v1/reference/leaves/delete-leave_requests) # List all leave types (/company/v1/reference/leaves/get-leave_types) # Create a leave type (/company/v1/reference/leaves/post-leave_types) # Get a leave type (/company/v1/reference/leaves/get-leave_types-id) # Delete a leave type (/company/v1/reference/leaves/delete-leave_types-id) # Update a leave type (/company/v1/reference/leaves/put-leave_types-id) # List all locations (/company/v1/reference/locations/get-locations) # Create a location (/company/v1/reference/locations/post-locations) # List all positions (/company/v1/reference/positions/get-positions) # Create a position (/company/v1/reference/positions/post-positions) # Delete a position (/company/v1/reference/positions/delete-positions-id) # Update a position (/company/v1/reference/positions/put-positions-id) # List all probation policies (/company/v1/reference/probation-policies/get-probation_policies) # List all applicant tags (/company/v1/reference/candidates/get-recruitment-applicant_tags) # List all candidate fields (/company/v1/reference/candidates/get-recruitment-candidate_fields) # List all candidates (/company/v1/reference/candidates/get-recruitment-candidates) # Create a candidate (/company/v1/reference/candidates/post-recruitment-candidates) # List all candidate educations (/company/v1/reference/candidates/get-recruitment-candidates-candidate_id-educations) # Create a candidate education (/company/v1/reference/candidates/post-recruitment-candidates-candidate_id-educations) # Delete a candidate education (/company/v1/reference/candidates/delete-recruitment-candidates-candidate_id-educations-id) # Update a candidate education (/company/v1/reference/candidates/put-recruitment-candidates-candidate_id-educations-id) # List candidate experiences (/company/v1/reference/candidates/get-recruitment-candidates-candidate_id-experiences) # Create a candidate experience (/company/v1/reference/candidates/post-recruitment-candidates-candidate_id-experiences) # Delete a candidate experience (/company/v1/reference/candidates/delete-recruitment-candidates-candidate_id-experiences-id) # Update a candidate experience (/company/v1/reference/candidates/put-recruitment-candidates-candidate_id-experiences-id) # Get a candidate (/company/v1/reference/candidates/get-recruitment-candidates-id) # Delete a candidate (/company/v1/reference/candidates/delete-recruitment-candidates-id) # Update a candidate (/company/v1/reference/candidates/put-recruitment-candidates-id) # List all sources (/company/v1/reference/candidates/get-recruitment-sources) # Create a source (/company/v1/reference/candidates/post-recruitment-sources) # Delete a source (/company/v1/reference/candidates/delete-recruitment-sources-id) # Update a source (/company/v1/reference/candidates/put-recruitment-sources-id) # List all disqualify reasons (/company/v1/reference/disqualify-reasons/get-recruitment-disqualify_reasons) # List all vacancy pipelines (/company/v1/reference/vacancies/get-recruitment-pipelines) # List all vacancies (/company/v1/reference/vacancies/get-recruitment-vacancies) # Create a vacancy (/company/v1/reference/vacancies/post-recruitment-vacancies) # Get a vacancy (/company/v1/reference/vacancies/get-recruitment-vacancies-id) # Delete a vacancy (/company/v1/reference/vacancies/delete-recruitment-vacancies-id) # Update a vacancy (/company/v1/reference/vacancies/put-recruitment-vacancies-id) # List all vacancy applications (/company/v1/reference/vacancies/get-recruitment-vacancies-id-applications) # Create a vacancy application (/company/v1/reference/vacancies/post-recruitment-vacancies-vacancy_id-applications) # Get a vacancy application (/company/v1/reference/vacancies/get-recruitment-vacancies-id-applications-application_id) # Update a vacancy application (/company/v1/reference/vacancies/put-recruitment-vacancies-vacancy_id-applications-id) # Disqualify a vacancy application (/company/v1/reference/vacancies/post-recruitment-vacancies-vacancy_id-applications-id-disqualify) # Move a vacancy application (/company/v1/reference/vacancies/put-recruitment-vacancies-vacancy_id-applications-id-move) # List all vacancy fields (/company/v1/reference/vacancies/get-recruitment-vacancy_fields) # List all vacancy tags (/company/v1/reference/vacancies/get-recruitment-vacancy_tags) # Get leave balances report (/company/v1/reference/reports/get-reports-hr-leave_balances) # Get working hours report (/company/v1/reference/reports/get-reports-hr-working_hours) # Get current session (/company/v1/reference/sessions/get-sessions-me) # List all tasks (/company/v1/reference/tasks/get-tasks) # Create a task (/company/v1/reference/tasks/post-tasks) # Complete a task (/company/v1/reference/tasks/put-tasks-id-complete) # Mark a task as incomplete (/company/v1/reference/tasks/put-tasks-id-incomplete) # List all teams (/company/v1/reference/teams/get-teams) # Create a team (/company/v1/reference/teams/post-teams) # Delete a team (/company/v1/reference/teams/delete-teams-id) # Update a team (/company/v1/reference/teams/put-teams-id) # Create a team member (/company/v1/reference/teams/post-teams-team_id-team_members) # Delete a team member (/company/v1/reference/teams/delete-teams-team_id-team_members-id) # List termination reasons (/company/v1/reference/termination-reasons/get-termination_reasons) # List all termination types (/company/v1/reference/termination-types/get-termination_types) # Create a webhook subscription (/company/v1/reference/webhooks/post-webhooks) # List webhook subscriptions (/company/v1/reference/webhooks/get-webhooks-list) # Delete a webhook subscription (/company/v1/reference/webhooks/delete-webhooks-id) # List workflows (/company/v1/reference/workflows/get-workflows) # Create an asset assignment (/company/v2/reference/assets/post-asset_assignments) # Delete an asset assignment (/company/v2/reference/assets/delete-asset_assignments-id) # Update an asset assignment (/company/v2/reference/assets/put-asset_assignments-id) # List all asset categories (/company/v2/reference/assets/get-asset_categories) # Create an asset category (/company/v2/reference/assets/post-asset_categories) # List all assets (/company/v2/reference/assets/get-assets) # Create an asset (/company/v2/reference/assets/post-assets) # Get an asset (/company/v2/reference/assets/get-assets-id) # Delete an asset (/company/v2/reference/assets/delete-assets-id) # Update an asset (/company/v2/reference/assets/put-assets-id) # List of audits (/company/v2/reference/audits/get-audits) # List all calendars (/company/v2/reference/calendars/get-calendars) # List all compensation types (/company/v2/reference/compensation-types/get-compensation_types) # List all competencies (/company/v2/reference/competencies/get-competencies) # List all departments (/company/v2/reference/departments/get-departments) # Create a department (/company/v2/reference/departments/post-departments) # Delete a department (/company/v2/reference/departments/delete-departments-id) # Update a department (/company/v2/reference/departments/put-departments-id) # List all divisions (/company/v2/reference/divisions/get-divisions) # Create a division (/company/v2/reference/divisions/post-divisions) # Delete a division (/company/v2/reference/divisions/delete-divisions-id) # Update a division (/company/v2/reference/divisions/put-divisions-id) # List all document folders (/company/v2/reference/document-folders/get-document_folders) # Create a document folder (/company/v2/reference/document-folders/post-document_folders) # Get a document folder (/company/v2/reference/document-folders/get-document_folder-id) # Delete a document folder (/company/v2/reference/document-folders/delete-document_folder-id) # Update a document folder (/company/v2/reference/document-folders/put-document-folder) # List all employee fields (/company/v2/reference/employees/get-employee-fields) # List all employee tables (/company/v2/reference/employees/list-employee-tables) # List all employees (/company/v2/reference/employees/get-employees) # Create an employee (/company/v2/reference/employees/post-employees) # List employee anniversaries (/company/v2/reference/employees/get-employees-anniversaries) # List employee birthdays (/company/v2/reference/employees/get-employees-birthdays) # List terminated employees (/company/v2/reference/employees/get-employees-terminated) # List employee additional compensations (/company/v2/reference/employees/get-employees-id-additional_compensations) # Create employee additional compensation (/company/v2/reference/employees/post-employees-employee_id-additional_compensations) # Delete employee additional compensation (/company/v2/reference/employees/delete-employees-id-additional_compensations) # List of employee certifications (/company/v2/reference/employees/get-employees-certifications) # List all employee salaries (/company/v2/reference/employees/get-employees-id-salaries) # List of employee documents (/company/v2/reference/employees/get-employees-documents) # Create an employee document (/company/v2/reference/employees/post-employee_documents) # Get document (/company/v2/reference/employees/get-employee-document) # Delete an employee document (/company/v2/reference/employees/delete-employees-employee_id-documents-id) # List of employee educations (/company/v2/reference/employees/get-employees-id-educations) # List an employee employment statuses (/company/v2/reference/employees/get-employees-id-employment_statuses) # Create an employee employment statuses (/company/v2/reference/employees/post-employees-id-employment_statuses) # Delete an employee employment status (/company/v2/reference/employees/delete-employees-employee_id-employment_statuses-id) # Update an employee employment status (/company/v2/reference/employees/put-employees-employee_id-employment_statuses-id) # List of employee field histories (/company/v2/reference/employees/get-employees-field-histories) # Create an employee note (/company/v2/reference/employees/post-employees-id-notes) # List of employee positions (/company/v2/reference/employees/get-employees-id-positions) # Create an employee position (/company/v2/reference/employees/post-employees-id-positions) # Delete an employee position (/company/v2/reference/employees/delete-employees-employee_id-positions-id) # Update an employee position (/company/v2/reference/employees/put-employees-employee_id-positions-id) # Create an employee salary (/company/v2/reference/employees/post-employees-id-salary) # Delete an employee salary (/company/v2/reference/employees/delete-employees-employee_id-salaries-id) # Update an employee salary (/company/v2/reference/employees/put-employees-id-salaries) # List of employee skills (/company/v2/reference/employees/list-employees-skills) # Create an employee skill (/company/v2/reference/employees/post-employees-skills) # Delete an employee skill (/company/v2/reference/employees/delete-employees-skills) # Update an employee skill (/company/v2/reference/employees/put-employees-skills) # Get an employee table (/company/v2/reference/employees/get-employee-table) # Get employee (/company/v2/reference/employees/get-employee) # Update an employee (/company/v2/reference/employees/put-employees-id) # Activate an employee (/company/v2/reference/employees/post-employees-id-activate) # List of employee assets (/company/v2/reference/employees/get-employees-id-assets) # List of employee dependents (/company/v2/reference/employees/get-employees-id-dependents) # List of employee emergency contacts (/company/v2/reference/employees/get-employees-id-emergency_contacts) # List of employee holidays (/company/v2/reference/employees/get-employees-id-holidays) # List of employee notes (/company/v2/reference/employees/get-employees-id-notes) # List of employee tasks (/company/v2/reference/employees/get-employees-id-tasks) # Terminate an employee (/company/v2/reference/employees/post-employees-id-terminate) # Update employee avatar (/company/v2/reference/employees/put-employees-id-update_avatar) # List of employee leave types (/company/v2/reference/leaves/get-employees-id-employee_leave_types) # Assign a leave policy to an employee (/company/v2/reference/leaves/post-employees-id-employee_leave_types) # Delete an employee leave type (/company/v2/reference/leaves/delete-employees-employee_id-leave_types-id) # List of employee leave balances (/company/v2/reference/leaves/get-employees-id-leave_balances) # Update an employee leave balance (/company/v2/reference/leaves/put-employees-id-update_leave_balance) # Create a leave adjustment (/company/v2/reference/leaves/create-leave-adjustment) # List all leave policies (/company/v2/reference/leaves/get-leave_policies) # List all leave requests (/company/v2/reference/leaves/get-leave-requests) # Create a leave request (/company/v2/reference/leaves/create-leave-request) # List all pending leave requests (/company/v2/reference/leaves/get-pending-leave-requests) # Get a leave request (/company/v2/reference/leaves/get-leave-request) # Delete a leave request (/company/v2/reference/leaves/delete-leave-request) # List all leave types (/company/v2/reference/leaves/get-leave_types) # List all employment types (/company/v2/reference/employment-types/get-employment_types) # Create an employment type (/company/v2/reference/employment-types/post-employment_types) # Delete an employment type (/company/v2/reference/employment-types/delete-employment_types-id) # Update an employment type (/company/v2/reference/employment-types/put-employment_types-id) # List all external users (/company/v2/reference/external-users/get-external_users) # List genders (/company/v2/reference/genders/list-genders) # Create gender (/company/v2/reference/genders/post-genders) # Delete gender (/company/v2/reference/genders/delete-genders-id) # Update gender (/company/v2/reference/genders/put-genders-id) # List all holiday policies (/company/v2/reference/holidays/get-holiday_policies) # Create a holiday policy (/company/v2/reference/holidays/post-holiday_policies) # Delete a holiday policy (/company/v2/reference/holidays/delete-holiday_policies-id) # Update a holiday policy (/company/v2/reference/holidays/put-holiday_policies-id) # List all holidays (/company/v2/reference/holidays/get-holidays) # Create a holiday (/company/v2/reference/holidays/post-holidays) # List all job levels (/company/v2/reference/job-levels/get-job_levels) # Get an article (/company/v2/reference/knowledge-base/get-knowledge_base-articles-article_id) # List all categories (/company/v2/reference/knowledge-base/get-knowledge_base) # List all articles (/company/v2/reference/knowledge-base/get-knowledge_base-categories-category_id-articles) # List all locations (/company/v2/reference/locations/get-locations) # Create a location (/company/v2/reference/locations/post-locations) # List all pay schedules (/company/v2/reference/pay-schedules/get-pay_schedules) # Get all key performance indicators (/company/v2/reference/kpi/get-key_performance_indicators) # Create a key performance indicator (/company/v2/reference/kpi/post-key_performance_indicators) # Create progress record (/company/v2/reference/kpi/post-key_performance_indicators-key_performance_indicator_id-key_performance_indicator_results) # List all objectives (/company/v2/reference/objectives/get-objectives) # List all review cycles (/company/v2/reference/reviews/review_cycles) # List all reviews (/company/v2/reference/reviews/get-performance-review_cycles-review_cycle_id-reviews) # List all review answers (/company/v2/reference/reviews/get-performance-review_cycles-review_cycle_id-reviews-review_id-answers) # List all review questions (/company/v2/reference/reviews/get-performance-review_cycles-review_cycle_id-reviews-review_id-questions) # List all review participants (/company/v2/reference/reviews/get-performance-review_cycles-review_cycle_id-reviews-review_id-review_participants) # List all positions (/company/v2/reference/positions/get-positions) # Create a position (/company/v2/reference/positions/post-positions) # Delete a position (/company/v2/reference/positions/delete-positions-id) # Update a position (/company/v2/reference/positions/put-positions-id) # List all probation policies (/company/v2/reference/probation-policies/get-probation_policies) # Create a probation policy (/company/v2/reference/probation-policies/post-probation_policies) # Delete probation policy (/company/v2/reference/probation-policies/delete-probation_policies-probation_policy_id) # Update probation policy (/company/v2/reference/probation-policies/put-probation_policies-probation_policy_id) # List applicant tags (/company/v2/reference/recruitment/list-recruitment-applicant-tags) # Get application (/company/v2/reference/recruitment/get-recruitment-vacancy-application) # List vacancy tags (/company/v2/reference/recruitment/list-recruitment-vacancy-tags) # List all candidate fields (/company/v2/reference/candidates/get-recruitment-candidate-fields) # List all candidates (/company/v2/reference/candidates/get-recruitment-candidates) # Create a candidate (/company/v2/reference/candidates/post-recruitment-candidates) # List all candidate experiences (/company/v2/reference/candidates/get-recruitment-candidates-candidate-experiences) # Create a candidate education (/company/v2/reference/candidates/post-recruitment-candidates-candidate-educations) # Delete a candidate education (/company/v2/reference/candidates/delete-recruitment-candidates-candidate_id-educations) # Update a candidate education (/company/v2/reference/candidates/put-recruitment-candidates-candidate_id-educations) # Create a candidate experience (/company/v2/reference/candidates/post-recruitment-candidates-candidate_id-experiences) # Delete a candidate experience (/company/v2/reference/candidates/delete-recruitment-candidates-candidate_id-experiences) # Update a candidate experience (/company/v2/reference/candidates/put-recruitment-candidates-candidate_id-experiences) # List all candidate notes (/company/v2/reference/candidates/get-recruitment-candidates-candidate-notes) # Create a candidate note (/company/v2/reference/candidates/post-recruitment-candidates-candidate-notes) # Get a candidate (/company/v2/reference/candidates/get-recruitment-candidate) # Delete a candidate (/company/v2/reference/candidates/delete-recruitment-candidates-id) # Update a candidate (/company/v2/reference/candidates/put-recruitment-candidates-id) # List all sources (/company/v2/reference/candidates/get-sources) # List all disqualify reasons (/company/v2/reference/disqualify-reasons/get-recruitment-disqualify_reasons) # List all vacancy pipelines (/company/v2/reference/vacancies/get-recruitment-pipelines) # List all vacancies (/company/v2/reference/vacancies/get-vacancies) # Create a vacancy (/company/v2/reference/vacancies/post-recruitment-vacancies) # Get a vacancy (/company/v2/reference/vacancies/get-recruitment-vacancy) # Delete a vacancy (/company/v2/reference/vacancies/delete-recruitment-vacancies-id) # Update a vacancy (/company/v2/reference/vacancies/put-recruitment-vacancies-id) # Get vacancy pipeline statistics (/company/v2/reference/vacancies/get-vacancy-pipeline-stats) # List all vacancy applications (/company/v2/reference/vacancies/get-recruitment-vacancies-vacancy_id-applications) # Create a vacancy application (/company/v2/reference/vacancies/post-recruitment-vacancies-vacancy_id-applications) # Update a vacancy application (/company/v2/reference/vacancies/put-recruitment-vacancies-vacancy_id-applications-id) # Disqualify a vacancy application (/company/v2/reference/vacancies/disqualify-vacancy-application) # Move a vacancy application (/company/v2/reference/vacancies/move-vacancy-application) # List all vacancy fields (/company/v2/reference/vacancies/get-vacancy-fields) # Get HR working hours report (/company/v2/reference/reports/get-hr-working-hours-report) # Get current session info (/company/v2/reference/sessions/get-session-me) # List all skills (/company/v2/reference/skills/get-skills) # Create a skill (/company/v2/reference/skills/post-skills) # Delete a skill (/company/v2/reference/skills/delete-skills-id) # List all tasks (/company/v2/reference/tasks/get-tasks) # Create a task (/company/v2/reference/tasks/post-tasks) # Complete a task (/company/v2/reference/tasks/put-tasks-id-complete) # Incomplete a task (/company/v2/reference/tasks/put-tasks-id-incomplete) # List all teams (/company/v2/reference/teams/get-teams) # Create a team (/company/v2/reference/teams/post-teams) # Delete a team (/company/v2/reference/teams/delete-teams-id) # Update a team (/company/v2/reference/teams/put-teams-id) # Create a team member (/company/v2/reference/teams/post-teams-team_id-team_members) # Delete a team member (/company/v2/reference/teams/delete-teams-team_id-team_members-id) # List termination reasons (/company/v2/reference/termination-reasons/get-termination_reasons) # Create termination reason (/company/v2/reference/termination-reasons/post-termination_reasons) # Delete termination reason (/company/v2/reference/termination-reasons/delete-termination_reasons-id) # Update termination reason (/company/v2/reference/termination-reasons/put-termination_reasons-id) # List all termination types (/company/v2/reference/termination-types/get-termination_types) # Create termination type (/company/v2/reference/termination-types/post-termination_types) # Delete termination type (/company/v2/reference/termination-types/delete-termination_types-termination_type_id) # Update termination type (/company/v2/reference/termination-types/put-termination_types-termination_type_id) # List projects (/company/v2/reference/time/get-time-projects) # Create a project (/company/v2/reference/time/post-time-projects) # Bulk assign employees to project (/company/v2/reference/time/post-time-projects-project_id-project_users) # List all timesheet entries (/company/v2/reference/time/get-time-timesheet_entries) # Create timesheet entry (/company/v2/reference/time/post-time-timesheet_entries) # Bulk create timesheet entries (/company/v2/reference/time/post-time-timesheet-entries-bulk) # Bulk delete timesheet entries (/company/v2/reference/time/delete-time-timesheet_entries-bulk) # Delete timesheet entry (/company/v2/reference/time/delete-time-timesheet_entries-id) # List attendance (/company/v2/reference/time/get-time-timesheets) # List webhooks (/company/v2/reference/webhooks/list-webhooks) # List workflows (/company/v2/reference/workflows/list-workflows) # List of working patterns (/company/v2/reference/working-patterns/get-working_patterns) # Create working pattern (/company/v2/reference/working-patterns/post-working_patterns) # Delete working pattern (/company/v2/reference/working-patterns/delete-working_patterns-working_pattern_id) # Update working pattern (/company/v2/reference/working-patterns/put-working_patterns-working_pattern_id) # Create asset assignment (/company/v3/reference/assets/create-asset-assignment) # Delete asset assignment (/company/v3/reference/assets/delete-asset-assignment) # Update asset assignment (/company/v3/reference/assets/update-asset-assignment) # List asset categories (/company/v3/reference/assets/list-asset-categories) # Create asset category (/company/v3/reference/assets/create-asset-category) # List assets (/company/v3/reference/assets/list-assets) # Create asset (/company/v3/reference/assets/create-asset) # Get asset (/company/v3/reference/assets/get-asset) # Delete asset (/company/v3/reference/assets/delete-asset) # Update asset (/company/v3/reference/assets/update-asset) # List audits (/company/v3/reference/audits/list-audits) # Get calendar (/company/v3/reference/calendars/get-calendar) # List compensation components (/company/v3/reference/compensation-components/get-compensation_components) # Create compensation component (/company/v3/reference/compensation-components/post-compensation_components) # Update compensation component (/company/v3/reference/compensation-components/patch-compensation_components-id) # Delete compensation component (/company/v3/reference/compensation-components/delete-compensation_components-id) # List compensation types (/company/v3/reference/compensation-types/list-compensation-types) # Create compensation type (/company/v3/reference/compensation-types/create-compensation-type) # Update compensation type (/company/v3/reference/compensation-types/update-compensation-type) # Delete compensation type (/company/v3/reference/compensation-types/delete-compensation-type) # List competencies (/company/v3/reference/competencies/list-competencies) # List departments (/company/v3/reference/departments/list-departments) # Create department (/company/v3/reference/departments/create-department) # Delete department (/company/v3/reference/departments/delete-department) # Update department (/company/v3/reference/departments/update-department) # List divisions (/company/v3/reference/divisions/list-divisions) # Create division (/company/v3/reference/divisions/create-division) # Delete division (/company/v3/reference/divisions/delete-division) # Update division (/company/v3/reference/divisions/update-division) # List document folders (/company/v3/reference/document-folders/list-document-folders) # Create document folder (/company/v3/reference/document-folders/create-document-folder) # Get document folder (/company/v3/reference/document-folders/get-document-folder) # Delete document folder (/company/v3/reference/document-folders/delete-document-folder) # Update document folder (/company/v3/reference/document-folders/update-document-folder) # List employee fields (/company/v3/reference/employees/list-employee-fields) # List options (/company/v3/reference/employees/list-employee-field-options) # Create option (/company/v3/reference/employees/create-employee-field-option) # Delete option (/company/v3/reference/employees/delete-employee-field-option) # Update option (/company/v3/reference/employees/update-employee-field-option) # List employee tables (/company/v3/reference/employees/list-employee-tables) # List columns (/company/v3/reference/employees/list-employee-table-columns) # List options (/company/v3/reference/employees/list-employee-table-column-options) # Create option (/company/v3/reference/employees/create-employee-table-column-option) # Delete option (/company/v3/reference/employees/delete-employee-table-column-option) # Update option (/company/v3/reference/employees/update-employee-table-column-option) # List employees (/company/v3/reference/employees/list-employees) # Create employee (/company/v3/reference/employees/create-employee) # List employee anniversaries (/company/v3/reference/employees/list-employee-anniversaries) # List employee birthdays (/company/v3/reference/employees/list-employee-birthdays) # Bulk update employees (/company/v3/reference/employees/bulk-update-employees) # Bulk update employee positions (/company/v3/reference/employees/bulk-update-employee-positions) # Bulk update employee salaries (/company/v3/reference/employees/bulk-update-employee-salaries) # List terminated employees (/company/v3/reference/employees/list-terminated-employees) # List additional compensations (/company/v3/reference/employees/list-employee-additional-compensations) # Create additional compensation (/company/v3/reference/employees/create-employee-additional-compensation) # Delete additional compensation (/company/v3/reference/employees/delete-employee-additional-compensation) # List certifications (/company/v3/reference/employees/list-employee-certifications) # List documents (/company/v3/reference/employees/list-employee-documents) # Create document (/company/v3/reference/employees/create-employee-document) # Get document (/company/v3/reference/employees/get-employee-document) # Delete document (/company/v3/reference/employees/delete-employee-document) # List educations (/company/v3/reference/employees/list-employee-educations) # List employment statuses (/company/v3/reference/employees/list-employee-employment-statuses) # Create employment status (/company/v3/reference/employees/create-employee-employment-status) # Delete employment status (/company/v3/reference/employees/delete-employee-employment-status) # Update employment status (/company/v3/reference/employees/update-employee-employment-status) # List field histories (/company/v3/reference/employees/list-employee-field-histories) # List job profiles (/company/v3/reference/employees/list-employee-job-profiles) # Create job profile (/company/v3/reference/employees/create-employee-job-profile) # Delete job profile (/company/v3/reference/employees/delete-employee-job-profile) # Update job profile (/company/v3/reference/employees/update-employee-job-profile) # List leave types (/company/v3/reference/employees/list-employee-leave-types) # Create leave type (/company/v3/reference/employees/create-employee-leave-type) # Delete leave type (/company/v3/reference/employees/delete-employee-leave-type) # Create note (/company/v3/reference/employees/create-employee-note) # List positions (/company/v3/reference/employees/list-employee-positions) # Create position (/company/v3/reference/employees/create-employee-position) # Delete position (/company/v3/reference/employees/delete-employee-position) # Update position (/company/v3/reference/employees/update-employee-position) # List salaries (/company/v3/reference/employees/list-employee-salaries) # Create salary (/company/v3/reference/employees/create-employee-salary) # Delete salary (/company/v3/reference/employees/delete-employee-salary) # Update salary (/company/v3/reference/employees/update-employee-salary) # List skills (/company/v3/reference/employees/list-employee-skills) # Create skill (/company/v3/reference/employees/create-employee-skill) # Delete skill (/company/v3/reference/employees/delete-employee-skill) # Update skill (/company/v3/reference/employees/update-employee-skill) # Get table (/company/v3/reference/employees/get-employee-table) # Create row (/company/v3/reference/employees/create-employee-table-row) # Delete row (/company/v3/reference/employees/delete-employee-table-row) # Update row (/company/v3/reference/employees/update-employee-table-row) # Get employee (/company/v3/reference/employees/get-employee) # Update employee (/company/v3/reference/employees/update-employee) # Activate employee (/company/v3/reference/employees/activate-employee) # List assets (/company/v3/reference/employees/list-employee-assets) # List dependents (/company/v3/reference/employees/list-employee-dependents) # List emergency contacts (/company/v3/reference/employees/list-employee-emergency-contacts) # List holidays (/company/v3/reference/employees/list-employee-holidays) # Leave balances employee (/company/v3/reference/employees/leave-balances-employee) # List notes (/company/v3/reference/employees/list-employee-notes) # List tasks (/company/v3/reference/employees/list-employee-tasks) # Terminate employee (/company/v3/reference/employees/terminate-employee) # Update employee avatar (/company/v3/reference/employees/update-employee-avatar) # Update leave balance employee (/company/v3/reference/employees/update-leave-balance-employee) # List employment types (/company/v3/reference/employment-types/list-employment-types) # Create employment type (/company/v3/reference/employment-types/create-employment-type) # Delete employment type (/company/v3/reference/employment-types/delete-employment-type) # Update employment type (/company/v3/reference/employment-types/update-employment-type) # List external users (/company/v3/reference/external-users/list-external-users) # List genders (/company/v3/reference/genders/list-genders) # Create gender (/company/v3/reference/genders/create-gender) # Delete gender (/company/v3/reference/genders/delete-gender) # Update gender (/company/v3/reference/genders/update-gender) # List holiday policies (/company/v3/reference/holidays/list-holiday-policies) # Create holiday policy (/company/v3/reference/holidays/create-holiday-policy) # Delete holiday policy (/company/v3/reference/holidays/delete-holiday-policy) # Update holiday policy (/company/v3/reference/holidays/update-holiday-policy) # List holidays (/company/v3/reference/holidays/list-holidays) # Create holiday (/company/v3/reference/holidays/create-holiday) # List job groups (/company/v3/reference/job-groups/list-job-groups) # Create job group (/company/v3/reference/job-groups/create-job-group) # Delete job group (/company/v3/reference/job-groups/delete-job-group) # Update job group (/company/v3/reference/job-groups/update-job-group) # List job levels (/company/v3/reference/job-levels/list-job-levels) # List job profiles (/company/v3/reference/job-profiles/list-job-profiles) # Create job profile (/company/v3/reference/job-profiles/create-job-profile) # Get job profile (/company/v3/reference/job-profiles/get-job-profile) # Delete job profile (/company/v3/reference/job-profiles/delete-job-profile) # Update job profile (/company/v3/reference/job-profiles/update-job-profile) # Get article (/company/v3/reference/knowledge-base/get-knowledge-base-article) # List categories (/company/v3/reference/knowledge-base/list-knowledge-base-categories) # List articles (/company/v3/reference/knowledge-base/list-knowledge-base-category-articles) # Create leave adjustment (/company/v3/reference/leaves/create-leave-adjustment) # List leave policies (/company/v3/reference/leaves/list-leave-policies) # List leave requests (/company/v3/reference/leaves/list-leave-requests) # Create leave request (/company/v3/reference/leaves/create-leave-request) # Pending leave request (/company/v3/reference/leaves/pending-leave-request) # Get leave request (/company/v3/reference/leaves/get-leave-request) # Delete leave request (/company/v3/reference/leaves/delete-leave-request) # List leave types (/company/v3/reference/leaves/list-leave-types) # List legal entities (/company/v3/reference/legal-entities/list-legal-entities) # List locations (/company/v3/reference/locations/list-locations) # Create location (/company/v3/reference/locations/create-location) # List pay schedules (/company/v3/reference/pay-schedules/list-pay-schedules) # List key performance indicators (/company/v3/reference/kpi/list-performance-key-performance-indicators) # Create key performance indicator (/company/v3/reference/kpi/create-performance-key-performance-indicator) # Create key performance indicator result (/company/v3/reference/kpi/create-performance-key-performance-indicator-key-performance-indicator-result) # List objectives (/company/v3/reference/objectives/list-performance-objectives) # List review cycles (/company/v3/reference/reviews/list-performance-review-cycles) # List reviews (/company/v3/reference/reviews/list-performance-review-cycle-reviews) # List answers (/company/v3/reference/reviews/list-performance-review-cycle-review-answers) # List questions (/company/v3/reference/reviews/list-performance-review-cycle-review-questions) # List review participants (/company/v3/reference/reviews/list-performance-review-cycle-review-review-participants) # List positions (/company/v3/reference/planning/list-planning-positions) # Create position (/company/v3/reference/planning/create-planning-position) # Delete position (/company/v3/reference/planning/delete-planning-position) # Update position (/company/v3/reference/planning/update-planning-position) # List positions (/company/v3/reference/positions/list-positions) # Create position (/company/v3/reference/positions/create-position) # Delete position (/company/v3/reference/positions/delete-position) # Update position (/company/v3/reference/positions/update-position) # List probation policies (/company/v3/reference/probation-policies/list-probation-policies) # Create probation policy (/company/v3/reference/probation-policies/create-probation-policy) # Delete probation policy (/company/v3/reference/probation-policies/delete-probation-policy) # Update probation policy (/company/v3/reference/probation-policies/update-probation-policy) # List candidate fields (/company/v3/reference/candidates/list-recruitment-candidate-fields) # List candidates (/company/v3/reference/candidates/list-recruitment-candidates) # Create candidate (/company/v3/reference/candidates/create-recruitment-candidate) # Create document (/company/v3/reference/candidates/create-recruitment-candidate-document) # List educations (/company/v3/reference/candidates/list-recruitment-candidate-educations) # Create education (/company/v3/reference/candidates/create-recruitment-candidate-education) # Delete education (/company/v3/reference/candidates/delete-recruitment-candidate-education) # Update education (/company/v3/reference/candidates/update-recruitment-candidate-education) # List experiences (/company/v3/reference/candidates/list-recruitment-candidate-experiences) # Create experience (/company/v3/reference/candidates/create-recruitment-candidate-experience) # Delete experience (/company/v3/reference/candidates/delete-recruitment-candidate-experience) # Update experience (/company/v3/reference/candidates/update-recruitment-candidate-experience) # List notes (/company/v3/reference/candidates/list-recruitment-candidate-notes) # Create note (/company/v3/reference/candidates/create-recruitment-candidate-note) # Get candidate (/company/v3/reference/candidates/get-recruitment-candidate) # Delete candidate (/company/v3/reference/candidates/delete-recruitment-candidate) # Update candidate (/company/v3/reference/candidates/update-recruitment-candidate) # List movements (/company/v3/reference/candidates/list-recruitment-movements) # List sources (/company/v3/reference/candidates/list-recruitment-sources) # List disqualify reasons (/company/v3/reference/disqualify-reasons/list-recruitment-disqualify-reasons) # List pipelines (/company/v3/reference/vacancies/list-recruitment-pipelines) # List vacancies (/company/v3/reference/vacancies/list-recruitment-vacancies) # Create vacancy (/company/v3/reference/vacancies/create-recruitment-vacancy) # Get vacancy (/company/v3/reference/vacancies/get-recruitment-vacancy) # Delete vacancy (/company/v3/reference/vacancies/delete-recruitment-vacancy) # Update vacancy (/company/v3/reference/vacancies/update-recruitment-vacancy) # Pipeline vacancy (/company/v3/reference/vacancies/pipeline-recruitment-vacancy) # List applications (/company/v3/reference/vacancies/list-recruitment-vacancy-applications) # Create application (/company/v3/reference/vacancies/create-recruitment-vacancy-application) # Get application (/company/v3/reference/vacancies/get-recruitment-vacancy-application) # Update application (/company/v3/reference/vacancies/update-recruitment-vacancy-application) # Disqualify application (/company/v3/reference/vacancies/disqualify-recruitment-vacancy-application) # Move application (/company/v3/reference/vacancies/move-recruitment-vacancy-application) # List vacancy fields (/company/v3/reference/vacancies/list-recruitment-vacancy-fields) # List people data changes (/company/v3/reference/reports/list-reports-people-data-changes) # List skills (/company/v3/reference/skills/list-skills) # Create skill (/company/v3/reference/skills/create-skill) # Delete skill (/company/v3/reference/skills/delete-skill) # List tasks (/company/v3/reference/tasks/list-tasks) # Create task (/company/v3/reference/tasks/create-task) # Complete task (/company/v3/reference/tasks/complete-task) # Incomplete task (/company/v3/reference/tasks/incomplete-task) # List teams (/company/v3/reference/teams/list-teams) # Create team (/company/v3/reference/teams/create-team) # Delete team (/company/v3/reference/teams/delete-team) # Update team (/company/v3/reference/teams/update-team) # Create team member (/company/v3/reference/teams/create-team-team-member) # Delete team member (/company/v3/reference/teams/delete-team-team-member) # List termination reasons (/company/v3/reference/termination-reasons/list-termination-reasons) # Create termination reason (/company/v3/reference/termination-reasons/create-termination-reason) # Delete termination reason (/company/v3/reference/termination-reasons/delete-termination-reason) # Update termination reason (/company/v3/reference/termination-reasons/update-termination-reason) # List termination types (/company/v3/reference/termination-types/list-termination-types) # Create termination type (/company/v3/reference/termination-types/create-termination-type) # Delete termination type (/company/v3/reference/termination-types/delete-termination-type) # Update termination type (/company/v3/reference/termination-types/update-termination-type) # List overtime requests (/company/v3/reference/time/list-time-overtime-requests) # Create overtime request (/company/v3/reference/time/create-time-overtime-request) # Get overtime request (/company/v3/reference/time/get-time-overtime-request) # Delete overtime request (/company/v3/reference/time/delete-time-overtime-request) # Update overtime request (/company/v3/reference/time/update-time-overtime-request) # List projects (/company/v3/reference/time/list-time-projects) # Create project (/company/v3/reference/time/create-time-project) # Create project user (/company/v3/reference/time/create-time-project-project-user) # List timesheet entries (/company/v3/reference/time/list-time-timesheet-entries) # Create timesheet entry (/company/v3/reference/time/create-time-timesheet-entry) # Create bulk (/company/v3/reference/time/create-time-timesheet-entry-bulk) # Delete bulk (/company/v3/reference/time/delete-time-timesheet-entry-bulk) # Delete timesheet entry (/company/v3/reference/time/delete-time-timesheet-entry) # List timesheets (/company/v3/reference/time/list-time-timesheets) # List working patterns (/company/v3/reference/working-patterns/list-working-patterns) # Create working pattern (/company/v3/reference/working-patterns/create-working-pattern) # Delete working pattern (/company/v3/reference/working-patterns/delete-working-pattern) # Update working pattern (/company/v3/reference/working-patterns/update-working-pattern) # List all departments (/company/v4/reference/core/getApiV4Departments) Returns a list of departments # Create a department (/company/v4/reference/core/postApiV4Departments) Creates a new department # Get a department (/company/v4/reference/core/getApiV4DepartmentsId) Returns a single department # Delete a department (/company/v4/reference/core/deleteApiV4DepartmentsId) Deletes a department # Update a department (/company/v4/reference/core/putApiV4DepartmentsId) Updates a department # List all divisions (/company/v4/reference/core/getApiV4Divisions) Returns a list of divisions # Create a division (/company/v4/reference/core/postApiV4Divisions) Creates a new division # Get a division (/company/v4/reference/core/getApiV4DivisionsId) Returns a single division # Delete a division (/company/v4/reference/core/deleteApiV4DivisionsId) Deletes a division # Update a division (/company/v4/reference/core/putApiV4DivisionsId) Updates a division # List all work types (/company/v4/reference/core/getApiV4WorkTypes) Returns a list of work types # Create a work type (/company/v4/reference/core/postApiV4WorkTypes) Creates a new work type # Get a work type (/company/v4/reference/core/getApiV4WorkTypesId) Returns a single work type # Delete a work type (/company/v4/reference/core/deleteApiV4WorkTypesId) Deletes a work type # Update a work type (/company/v4/reference/core/putApiV4WorkTypesId) Updates a work type # Create a job level (/company/v4/reference/core/postApiV4JobLevels) Create a job level # Delete a job level (/company/v4/reference/core/deleteApiV4JobLevelsId) Delete a job level # Update a job level (/company/v4/reference/core/putApiV4JobLevelsId) Update a job level # Delete a location (/company/v4/reference/core/deleteApiV4LocationsId) Delete a location # Update a location (/company/v4/reference/core/putApiV4LocationsId) Update a location # List all job titles (/company/v4/reference/core/getApiV4JobTitles) Return a list of job titles # Create a job title (/company/v4/reference/core/postApiV4JobTitles) Create a new job title # Delete a job title (/company/v4/reference/core/deleteApiV4JobTitlesId) Delete a job title # Update a job title (/company/v4/reference/core/putApiV4JobTitlesId) Update a job title # List all job levels (/company/v4/reference/job_levels/getApiV4JobLevels) Returns a list of job levels # List all locations (/company/v4/reference/locations/getApiV4Locations) List all locations # Create a location (/company/v4/reference/locations/postApiV4Locations) Create a new location # List all people (/company/v4/reference/people/getApiV4People) Returns a list of people # Create a person (/company/v4/reference/people/postApiV4People) Create a person # List all terminated people (/company/v4/reference/people/getApiV4PeopleTerminated) Returns a list of terminated people # List work anniversaries (/company/v4/reference/people/getApiV4PeopleAnniversaries) Returns a list of work anniversaries for people # List birthdays (/company/v4/reference/people/getApiV4PeopleBirthdays) Returns a list of birthdays for people # Get a person (/company/v4/reference/people/getApiV4PeopleId) Returns a single person # Update a person (/company/v4/reference/people/putApiV4PeopleId) Update a person # Update a person's avatar (/company/v4/reference/people/putApiV4PeopleIdAvatar) Update a person's avatar # Terminate a person (/company/v4/reference/people/postApiV4PeopleIdTerminate) Terminate a person # Activate a person (/company/v4/reference/people/postApiV4PeopleIdActivate) Activate a person # List a person's assets (/company/v4/reference/people/getApiV4PeopleIdAssets) Returns a list of person assets # List all person salaries (/company/v4/reference/people/getApiV4PeoplePersonIdCompensationSalaries) Returns a list of salary records for a person # Get a person salary (/company/v4/reference/people/getApiV4PeoplePersonIdCompensationSalariesId) Returns a single salary record # Delete a person salary (/company/v4/reference/people/deleteApiV4PeoplePersonIdCompensationSalariesId) Delete a person salary record # Update a person salary (/company/v4/reference/people/putApiV4PeoplePersonIdCompensationSalariesId) Update a person salary record # List person lifecycle records (/company/v4/reference/people/getApiV4PeoplePersonIdLifecycles) Returns a list of lifecycle records for a person # List all objectives (/company/v4/reference/perform/getApiV4PerformObjectives) Returns a list of objectives # Get an objective (/company/v4/reference/perform/getApiV4PerformObjectivesId) Returns a single objective # List all review cycles (/company/v4/reference/perform/getApiV4PerformReviewCycles) Returns a list of review cycles # List all review responses (/company/v4/reference/perform/getApiV4PerformReviewResponses) Returns a list of review responses # List all lifecycle surveys (/company/v4/reference/pulse/getApiV4PulseLifecycleSurveys) Returns a list of lifecycle surveys # List all lifecycle survey responses (/company/v4/reference/pulse/getApiV4PulseLifecycleSurveyResponses) Returns a list of lifecycle survey responses # List all engagement surveys (/company/v4/reference/pulse/getApiV4PulseEngagementSurveys) Returns a list of engagement surveys # List all engagement survey responses (/company/v4/reference/pulse/getApiV4PulseEngagementSurveyResponses) Returns a list of engagement survey responses # Get Api Careers V1 Vacancies (/careers/v1/reference/vacancies/getApiCareersV1Vacancies) Returns a list of open vacancies # Get Api Careers V1 Vacancies Id (/careers/v1/reference/vacancies/getApiCareersV1VacanciesId) Returns a single vacancy. # Get Api Careers V1 Locations (/careers/v1/reference/locations/getApiCareersV1Locations) Returns a list of locations # Get Api Careers V1 Employment Types (/careers/v1/reference/employment_types/getApiCareersV1EmploymentTypes) Returns a list of employment types