Official 100Hires ATS plugin — manage candidates, jobs, applications, interviews, and messages from your AI editor. 131 MCP tools, OAuth auth, no API key setup.
# 100Hires ATS — rules for AI agents
You have access to the 100Hires MCP server (131 tools). The rules below match the actual tool schemas — never guess parameter names, enums, or formats. When a value isn't in the rules, call the corresponding `hires_list_*` tool to discover it.
## Authentication
The server uses OAuth 2.0 (RFC 8414 / RFC 9728 + PKCE + Dynamic Client Registration). The editor opens a browser on first use. If a tool call returns 401, ask the user to re-authenticate via the editor's plugin settings.
## ID vs alias
| Entity | Accepts |
|---|---|
| Candidate | numeric ID **or** string alias (param `id`) |
| Job | numeric ID **or** string alias (param `id`) |
| Application | **numeric ID only** (param `id`) |
| Notes, interviews, messages, evaluations, forms, templates, webhooks | numeric ID only |
When the user provides a name (not an ID), first call `hires_list_candidates(q=...)`, `hires_list_jobs(...)`, or `hires_list_users(search=...)` to resolve to an ID.
## Date / time formats
- **Unix timestamp in seconds (integer)** — used everywhere except as noted below: `scheduled_at`, `start_time`, `end_time`, `send_at`, `date_from`, `date_to`, `created_after`, `updated_after`, `since`, `until`, `moved_at`.
- **`YYYY-MM-DD` string** — only `hires_list_interviews(date=...)`.
- **ISO 8601 duration** — only `assign_task` nurture step `due_in_interval` (e.g. `"P3D"`).
- **Never ISO datetime strings** for scheduling.
## File / attachment payloads
All file fields (`cv`, `resume`, `file`, voicemail, company logo) take the SAME object shape — **never** a URL or file path:
```json
{
"data": "<base64>",
"file_name": "resume.pdf",
"mime_type": "application/pdf",
"size": 123456
}
```
`size` is optional. `data`, `file_name`, `mime_type` are required.
## Read before write
Before any destructive call (`delete_*`, `disqualify_candidate`, `batch_reject_*`, `batch_move_*`, `reject_application`, `hire_application`):
1. Call the corresponding `list_*` or `get_*` to confirm state.
2. Show the user what will change.
3. Then mutate.
`hires_disqualify_candidate` rejects **all active applications** for the candidate — confirm with the user explicitly.
## Candidates
- `hires_create_candidate` — every field is optional (`first_name`, `last_name`, `email`, `phone`, `profile`, `job_id`, `stage_id`, `cv`). If `stage_id` is set, `job_id` is also required.
- `profile` field: object of `{question_text_or_id: string_value}` — record of strings, not nested objects.
- `hires_list_candidates` filters: `job_id`, `stage_id`, `email` (exact), `q` (partial name/email), `full_name`, `linkedin`, `created_after`, `updated_after` (Unix seconds), `include` (string), `page`, `size`.
- `hires_add_candidate_tags(tags=["..."])` — array of **tag strings**, not IDs.
- `hires_remove_candidate_tag(tag="...")` — singular `tag` string, not array.
- `hires_batch_add_tags(ids=[1,2], tags=["a","b"])` — `ids` are **numeric only** (no aliases). Max 100.
- `hires_list_candidate_activities(event_type=...)` — comma-separated string from: `comment`, `copilot_response`, `stage_moved`, `automation_action_triggered`, `assign_job`, `enrichment`, `call`, `validate_emails`, `profile_mutation`, `qualification`, `assign_tags`, `assign_sources`, `candidate_rate`.
- `hires_list_candidate_messages(is_scheduled=1)` — literal integer `1`, not boolean `true`, not string `"1"`.
- `hires_disqualify_candidate(reasons=[id...])` — optional. IDs from `hires_list_rejection_reasons`.
- `hires_get_candidate_resume(include="text_content")` — adds `text` field with parsed plain text; no download needed.
### Sending messages
`hires_send_candidate_message(id=<candidate>, to=[...], subject, body, scheduled_at?, from_account_id?, reply_to_email_id?, send_in_new_thread?)`:
- **Required:** `to` (array of strings), `subject`, `body` (HTML).
- `scheduled_at` — Unix seconds. Defaults to **created_time + 900 seconds (15 min)** if omitted.
- `from_account_id` — numeric mail account ID. **There is no `from_user_id` param.** To find an account ID:
- `hires_get_user(id).default_mail_account_id`, OR
- `hires_list_user_mail_accounts(id=<user_id>)`, OR
- `hires_list_company_mail_accounts` (current company), OR
- `hires_list_company_id_mail_accounts(id=<company_id>)`.
- `reply_to_email_id` — mailbox message ID for threading. **Not** `reply_to_message_id`.
## Applications
`id` is **numeric only**. Aliases not accepted.
- `hires_create_application(candidate_id, job_id, stage_id?)` — `candidate_id` accepts alias or number, `job_id` is numeric. `stage_id` optional (defaults to first stage).
- `hires_list_applications` filters: `candidate_id`, `job_id`, `stage_id`, `status` (`"pending"` | `"hired"` | `"rejected"`), `sort` (`"created_at"` | `"-created_at"` | `"ai_score"` | `"-ai_score"`), `include` (`candidate`, `cv.text` — comma-separated).
- `hires_move_application(id, stage_id)` — `stage_id` is **required**, sets a specific stage. Use `hires_list_workflow_stages(job_id=...)` or `hires_get_workflow_stages(id=<workflow_id>)` to discover IDs.
- `hires_advance_application(id)` — no `stage_id`; system picks the next stage from the workflow.
- `hires_hire_application(id)` — no body params.
- `hires_reject_application(id, rejection_reason_id?, suppress_notification?)` — both optional. `rejection_reason_id` from `hires_list_rejection_reasons`.
- `hires_transfer_application(id, job_id, stage_id?)` — creates a **new** application on target job.
- `hires_batch_move_applications(ids=[...], stage_id)` — max 100, numeric IDs only.
- `hires_batch_reject_applications(ids=[...], rejection_reason_id?)` — max 100.
- `hires_list_application_stage_history(id)` — returns `from_stage_id`, `to_stage_id`, `moved_at` (Unix seconds), `moved_by_type` (`system` | `user` | `automation`), `moved_by_user_id`.
- `hires_get_ai_score(id)` — returns null if not yet AI-scored.
- `hires_list_application_evaluations(id)` — returns evaluation form IDs. Use those with `hires_get_evaluation(id=...)`.
- `hires_upload_application_attachment(id, file={data,file_name,mime_type,size?})` — param is `file`, not `attachment`.
### Creating interviews
`hires_create_interview` lives in **applications.ts** (despite the name) and is called via `POST /applications/{id}/interviews`.
**Schema:**
- `id` — application ID (number, **path param**, not a body field).
- `start_time` — Unix seconds (required).
- `end_time` — Unix seconds (required).
- `interviewer_ids` — array of user IDs (required). **Not** `interviewer_user_id` (singular). **Not** a string.
- `location` — optional string.
- `include` — `candidate`, `application`, `job` (comma-separated).
**There is no `scheduled_at`, `duration_minutes`, `type`, or `interviewer_user_id` field on this tool.**
## Jobs
- `hires_create_job` — required: `status`, `title`, `description`, `location_city`, `location_country`. Everything else optional.
- `status` is a **free string** (label like `"Public"`, `"Draft"`, `"Archived"`). Get valid values from `hires_list_statuses`.
- `resume_field_status`: `"required"` | `"optional"` | `"hidden"`.
- `salary_period`: `"annually"` | `"monthly"` | `"daily"` | `"hourly"`.
- `knockout_questions[]` — each item: `text` (string), `expected_answer` (`"Yes"` | `"No"`), `disqualify_on_wrong_answer` (boolean). All three required per item.
- `ai_scoring_criteria[]` — each item: `id` (optional, for updates), `title` (optional), `text` (required), `weight` (required, int 1–10). **Diff-replace semantics**: items without `id` are created, items with `id` are updated, existing criteria not in the payload are removed. Pass `[]` to remove all.
- Workflow / form auto-creation: if `workflow_id` is omitted on create, a new workflow named after the job title is created with default stages. Same for `form_id`.
- `hires_set_job_status(id, status)` — separate **PATCH** endpoint from `hires_update_job`.
- `hires_publish_to_job_board(id, boards?)` — `boards` is array of string identifiers from `hires_list_boards` (e.g. `["indeed", "linkedin"]`). Optional.
- `hires_batch_publish_to_boards(jobs=[...], boards?)` / `hires_batch_remove_from_boards(jobs=[...], boards?)` — `jobs` is array of numeric job IDs.
- `hires_add_hiring_team_member(id, user_id)` — one user at a time.
- `hires_create_job_webhook` / `hires_delete_job_webhook` — `url` must be HTTPS. `hires_delete_job_webhook` takes both `id` (job ID) and `webhook_id`.
## Workflow stages
Two ways to get stage IDs for a job:
- `hires_list_workflow_stages(job_id=<job_id>)` — direct.
- `hires_get_workflow_stages(id=<workflow_id>)` — when you already know the workflow. Param is `id`, not `workflow_id`.
Stage IDs feed `hires_move_application(stage_id)`, `hires_batch_move_applications(stage_id)`, and nurture campaign `move_to_next_stage` steps.
**Stage IDs differ per workflow** — never reuse a stage_id across jobs without verifying.
## Messages (outbound mail)
- `hires_list_messages(from_account_id, status?, date_from?, date_to?)` — `from_account_id` is **required**. `status`: `"scheduled"` | `"sent"` | `"all"`. Dates are Unix seconds.
- `hires_update_message(id, to, subject, body, scheduled_at?, ...)` — PUT, full replace. `to`/`subject`/`body` required.
- `hires_patch_message(id, ...)` — PATCH, all fields optional.
- `hires_delete_message(id)` — cancels a scheduled message; cannot cancel already-sent.
- `hires_batch_create_messages(messages=[...])` — max 100. Each item: `candidate_id`, `to`, `subject`, `body` (required); `scheduled_at`, `from_account_id`, `reply_to_email_id`, `send_in_new_thread` optional.
## Notification messages
Separate from outbound messages — these are system emails (rejection notifications, etc.).
- `hires_update_notification_message(id, subject, body, scheduled_at?)` — only works for scheduled (not yet sent) notifications. `subject` and `body` required.
- `hires_cancel_all_notification_messages(candidate_id)` — cancels every pending notification for that candidate. Returns success even if none exist.
## Nurture campaigns
`hires_create_nurture_campaign(title, steps)` and `hires_update_nurture_campaign(id, title, steps)` — `title` is **required** in both.
**Update semantics**: send the entire `steps` array. Steps with `id` are updated, without `id` are created, marked `is_deleted=true` are removed.
**Every step requires** `delay_days` (≥0) and `send_condition` (`"if_no_reply"` | `"if_no_reply_but_opened"`). `type` selects the variant:
| `type` | Required step-specific fields |
|---|---|
| `email` | `sender: {type: "account"|"user", id: number}`, `template_id` (number) |
| `sms` | `sender_user_id` (number), `template_id` (number) |
| `voicemail` | `attachment_uuid` (from `hires_upload_attachment(category="voicemail")`) |
| `move_to_next_stage` | `stage_id` (number) |
| `assign_tag` | `tag_id` (number — **not** tag string) |
| `assign_task` | `task_template_id` (number), `assignees` (array of user IDs). Optional: `due_in_interval` (ISO duration), `priority` (number) |
## Attachments
- `hires_download_attachment(url)` — `url` must be absolute and **same-host as the configured 100Hires API** (other hosts rejected to prevent Bearer leakage). Max 25 MB. Returns `{file_name, mime_type, size, data: base64}`.
- `hires_upload_attachment(category, file, object_id?)` — `category` enum: `"voicemail"` | `"candidate"` | `"application"` | `"candidate_comment"` | `"job_note"` | `"company_favicon"` | `"company_header"` | `"company_link_preview"`. `object_id` is **omitted for `voicemail`**, required otherwise. Voicemail upload returns a `uuid` usable as `attachment_uuid` in nurture steps.
## Notes
- `hires_create_note(candidate_id, body, visibility?, mention_user_ids?, user_id?)` — `candidate_id` and `body` (HTML) required. `visibility`: `"all"` (default) or `"private"`. `mention_user_ids` notifies users via @-mention.
- `hires_update_note(id, body?, visibility?)` — both body and visibility optional.
## Forms
- `hires_create_form(name, questions?)` — `questions` is array of **question IDs** (numbers from `hires_list_questions`), not question objects.
- `hires_update_form(id, name, questions?)` — `name` is required even on update.
- `hires_update_form_question(form_id, question_id, status)` — `status`: `"required"` | `"optional"` | `"hidden"`.
## Email templates
- `hires_create_email_template(name, subject, body)` — all three required.
- `hires_update_email_template(id, ...)` — all fields optional; only provided fields are overwritten.
- Placeholders: `hires_list_template_placeholders(type)` (enum: `"profile_field"` | `"job_variable"` | `"questionnaire_link"` | `"self_scheduling_link"`) → `hires_prepare_template_placeholders` → insert returned HTML tag into `body`.
## Companies
- `hires_create_company` required: `name`, `company_owner_email`, `company_owner_name`. Logo is base64 object.
- `hires_list_company_mail_accounts` — no `id` param (uses authenticated company).
- `hires_list_company_id_mail_accounts(id)` — explicit company ID.
- Company webhooks (`hires_list_webhooks` / `hires_create_webhook` / `hires_delete_webhook`) are separate from job webhooks (`hires_list_job_webhooks` / `hires_create_job_webhook` / `hires_delete_job_webhook`).
## Career site (public, no auth)
`hires_list_career_jobs`, `hires_get_career_job`, `hires_submit_career_application` use `company_slug` (string) instead of API auth — they run on a separate unauthenticated client.
- `hires_get_career_job(company_slug, id)` — `id` is numeric. Returns 404 for non-public jobs (draft, archived, internal).
- `hires_submit_career_application` required: `company_slug`, `job_id`, `first_name`, `last_name`, `email`. Optional: `phone`, `resume` (base64 object), `linkedin_url`, `source`, `answers`.
## Evaluations & feedback
- `hires_get_evaluation(id)` — `id` is an evaluation form ID, **not** application ID. Find IDs via `hires_list_application_evaluations(id=<application_id>)`.
- `hires_submit_feedback(description, issue_type?)` — `issue_type`: `"missing_filter"` | `"pagination"` | `"performance"` | `"missing_field"` | `"bulk_operation"` | `"other"`. Rate-limited to 5/hour.
## Taxonomy (lookup tools)
When the user asks for an entity by name, resolve via the matching `hires_list_*` first:
| Need | Tool | Feeds |
|---|---|---|
| Rejection reason ID | `hires_list_rejection_reasons` | `reject_application.rejection_reason_id`, `batch_reject_applications.rejection_reason_id`, `disqualify_candidate.reasons[]` |
| Job status label | `hires_list_statuses` | `create_job.status`, `update_job.status`, `set_job_status.status`, `list_jobs.status` |
| Stage ID | `hires_list_workflow_stages(job_id=)` or `hires_get_workflow_stages(id=<workflow_id>)` | `move_application.stage_id`, `batch_move_applications.stage_id` |
| User ID | `hires_list_users(search=)` | `create_interview.interviewer_ids[]`, `add_hiring_team_member.user_id`, `create_note.mention_user_ids[]` |
| Mail account ID | `hires_get_user(id).default_mail_account_id` or `hires_list_user_mail_accounts` / `hires_list_company_mail_accounts` | `send_candidate_message.from_account_id` |
| Tag string | `hires_list_tags` | `add_candidate_tags.tags[]`, `batch_add_tags.tags[]` |
| Board identifier | `hires_list_boards` | `publish_to_job_board.boards[]` |
| Email template ID | `hires_list_email_templates` | nurture `email.template_id`, `sms.template_id` |
| Question ID | `hires_list_questions` | `create_form.questions[]`, `update_form_question.question_id` |
| Department ID | `hires_list_departments` | `create_job.department_id` |
| Question type | `hires_list_question_types` | `create_question.type` |
## Common workflows
**Move candidate(s) to a specific stage on a job:**
1. `hires_list_workflow_stages(job_id=<job_id>)` → pick `stage_id`.
2. `hires_list_applications(job_id=<job_id>, candidate_id=<candidate_id>)` → get `application_id`.
3. `hires_move_application(id=<application_id>, stage_id=<stage_id>)` — or `hires_batch_move_applications` for multiple.
**Schedule an interview:**
1. `hires_get_application(id=<id>)` → confirm candidate, job, current stage.
2. `hires_list_users(search="<name>")` → get interviewer user IDs.
3. Compute `start_time` and `end_time` as Unix seconds.
4. `hires_create_interview(id=<application_id>, start_time=<unix>, end_time=<unix>, interviewer_ids=[<user_id>...], location?)`.
**Reject with reason and email:**
1. `hires_list_rejection_reasons` → pick `rejection_reason_id`.
2. `hires_reject_application(id=<id>, rejection_reason_id=<id>, suppress_notification=false)`.
3. To abort the auto-sent notification: `hires_cancel_all_notification_messages(candidate_id=<id>)`.
**Send an email to a candidate:**
1. `hires_get_user(id=<sender_user_id>)` → read `default_mail_account_id`.
2. `hires_send_candidate_message(id=<candidate_id>, to=["candidate@..."], subject, body, from_account_id=<account_id>, scheduled_at?)`.