# Get merchant status Source: https://developer.jobmojito.com/api-reference/admin/get-merchant-status https://cool.jobmojito.com/functions/v1/openapi get /merchant-status Status snapshot for a merchant: interview-credit balances, subscription type/status, pending-work counts (undecided / ongoing / uncredited interviews), candidate & result totals with 14-day history, and invitation headroom. Scoped to your token's merchant (or a merchant_id override for admins / sub-merchant operators). Also echoes the caller's profile_id and default_merchant_id from the token, plus the effective merchant_id. # Invite admin / merchant-team or coaching users Source: https://developer.jobmojito.com/api-reference/admin/invite-admin-merchant-team-or-coaching-users https://cool.jobmojito.com/functions/v1/openapi post /invite-users Invites people to a merchant account and emails each one an invitation. Use it to add **admin / merchant-team users** who manage the recruiting dashboard (`merchant_owner`, `merchant`, `merchant_selection`), **coaching-portal users** (`user`, `mentor`), or **interview candidates** (`candidate`). Candidate invites require an `interview_id` and are gated by the merchant's available interview credits; admin and coaching invites are not. Each user in the array is processed independently and returned with an individual result, so one failure does not block the rest. Callable with a merchant JWT (merchant_owner/merchant/admin) or a service-role key (requires merchant_id). # List avatar templates Source: https://developer.jobmojito.com/api-reference/admin/list-avatar-templates https://cool.jobmojito.com/functions/v1/openapi get /merchant-avatar-list Paginated list of a merchant's avatar templates (the admin-portal avatars list), scoped to your token's merchant (or a merchant_id override). Capped at 1000 records per page. # List platform languages Source: https://developer.jobmojito.com/api-reference/admin/list-platform-languages https://cool.jobmojito.com/functions/v1/openapi get /platform-languages-list Returns all supported platform (mojito) languages: the mojito language code, English and local names, and an SVG flag image URL. Use the `code` as `mojito_language_code` when creating interviews or personas. # List sub-merchants Source: https://developer.jobmojito.com/api-reference/admin/list-sub-merchants https://cool.jobmojito.com/functions/v1/openapi get /merchant-sub-merchant-list Paginated list of the sub-merchants the caller administers (the admin-portal sub-merchants list). Visibility is enforced by row-level security. Capped at 1000 records per page. # List candidates Source: https://developer.jobmojito.com/api-reference/candidates/list-candidates https://cool.jobmojito.com/functions/v1/openapi get /merchant-candidate-list Paginated list of a merchant's candidates (the admin-portal candidates list), scoped to your token's merchant (or a merchant_id override). Capped at 1000 records per page. # Create a coaching catalogue directory Source: https://developer.jobmojito.com/api-reference/coaching-catalogue/create-a-coaching-catalogue-directory https://cool.jobmojito.com/functions/v1/openapi post /catalogue-tag-create Creates a directory (page) in the coaching portal catalogue. A directory nests other directories through `tags_sub`, lists coaching sessions through `tags_interview_set_filter`, and can replace the default grid with a custom Markdown page through `content_md`. The id you choose is the catalogue URL segment and cannot be changed afterwards. # Get a coaching catalogue directory Source: https://developer.jobmojito.com/api-reference/coaching-catalogue/get-a-coaching-catalogue-directory https://cool.jobmojito.com/functions/v1/openapi get /catalogue-tag-get Reads one catalogue directory in full: its settings, its custom Markdown page (`content_md`), the sub-directories it nests, and the coaching sessions its `tags_interview_set_filter` currently matches. Read a directory before updating it — `content_md`, `tags_sub` and `tags_interview_set_filter` are replaced wholesale by catalogue-tag-update, so you need the current value to extend rather than overwrite it. # List coaching catalogue directories Source: https://developer.jobmojito.com/api-reference/coaching-catalogue/list-coaching-catalogue-directories https://cool.jobmojito.com/functions/v1/openapi get /catalogue-tag-list Paginated list of the coaching-catalogue directories visible to you: your merchant's own, plus the platform-wide `public` ones unless you set include_public=false. Use it to find a directory id before updating one, to pick a `parent_tag`, or to walk the tree with `parent_tag`. The custom Markdown page is not included — fetch it per directory with catalogue-tag-get. # Update a coaching catalogue directory Source: https://developer.jobmojito.com/api-reference/coaching-catalogue/update-a-coaching-catalogue-directory https://cool.jobmojito.com/functions/v1/openapi post /catalogue-tag-update Updates a directory (page) of the coaching portal catalogue. Only the fields present in the request body are written — everything else keeps its current value, and sending null clears a nullable field. Use it to rename a directory, re-point which sessions it lists (`tags_interview_set_filter`), re-order or replace its sub-directories (`tags_sub`), author its custom Markdown page (`content_md`), or add, replace and remove its other language versions (`translations`). # Create a new interview Source: https://developer.jobmojito.com/api-reference/interviews/create-a-new-interview https://cool.jobmojito.com/functions/v1/openapi post /job-interview-create Creates a new interview / coaching / assessment definition, generates its description, questions and candidate expectations via AI, and provisions default steps. Optionally provisions an embed key. # Create a role-play — coaching persona or interview-portal role-play (choose with `portal`) Source: https://developer.jobmojito.com/api-reference/interviews/create-a-role-play-—-coaching-persona-or-interview-portal-role-play-choose-with-`portal` https://cool.jobmojito.com/functions/v1/openapi post /persona-create FIRST DECIDE `portal`. This endpoint creates two different products and the default is NOT the recruiter one: - `portal: "interview"` — an INTERVIEW ROLE-PLAY. Use this whenever the goal is to ASSESS or SCREEN candidates: hiring, assessments, sales role-plays for job applicants, anything a recruiter runs. Candidates are invited through the normal invitation flow, results appear in the recruiter's result list, it is billed against merchant credits, and attempts are capped via `interview_attempts` — exactly like an interview. - `portal: "coaching"` (THE DEFAULT) — a coaching persona for practice/training on the coaching portal. Consumed against the mentee's own coaching credits, self-started from the catalogue, and its results are NOT visible to recruiters. Omitting `portal` gives you this one, so pass `portal: "interview"` explicitly for any recruiting/assessment use case. The conversation itself behaves identically in both. The avatar plays a defined role in a free-form conversation instead of running a scored Q&A interview. No AI question/description generation runs; the persona role fields ARE the configuration. The session runs as: a welcome message → the avatar's OPENING LINE (the first thing it says, set via `opening_line`) → the candidate replies and the free-form role-play begins → a closing message. Because there is no question list, `opening_line` is how the avatar starts the scene — set it to a concrete in-character line; if omitted it defaults to a generic "Hello". Set `welcome_message` and `thank_you_message` too — omitting them leaves the generic platform defaults. Also set `candidate_expectations`: it is the yardstick the session is scored against. The four avatar prompts divide up as: `persona_avatar_who_is` (identity and what drives it), `persona_avatar_knowledge` (the private facts it may use), `persona_avatar_progress` (how the conversation is allowed to move forward, and what gates the later personal details), and `persona_avatar_end_conditions` (when to stop). Without `persona_avatar_progress` the avatar has no defined arc and tends to either concede immediately or never concede at all. Provisions the default conversational steps and optionally an embed key. # Create an interview for a candidate and return an access token URL Source: https://developer.jobmojito.com/api-reference/interviews/create-an-interview-for-a-candidate-and-return-an-access-token-url https://cool.jobmojito.com/functions/v1/openapi post /job-interview-create-for-candidate-with-token Creates (or resolves an existing) position and interview for a merchant, enrols the candidate, runs pre-screening, and returns a tokenised interview URL. Position fields are required only when position_def_set_id is not provided. # Create an interview from an array of questions Source: https://developer.jobmojito.com/api-reference/interviews/create-an-interview-from-an-array-of-questions https://cool.jobmojito.com/functions/v1/openapi post /job-interview-create-from-array Creates a new interview definition set from a caller-provided array of questions, builds its default and generated steps, optionally activates it, and optionally creates an embed key. # Generate a signed interview URL Source: https://developer.jobmojito.com/api-reference/interviews/generate-a-signed-interview-url https://cool.jobmojito.com/functions/v1/openapi post /job-interview-token Generates a public, token-signed URL for an existing interview, profile, or result. The required id fields depend on `type`. # Get interview definition Source: https://developer.jobmojito.com/api-reference/interviews/get-interview-definition https://cool.jobmojito.com/functions/v1/openapi get /job-interview-get Retrieves the interview definition for a given interview-definition id or position id. Returns the compiled `calc_definition_json`, the ordered `questions` array (in the same format job-interview-create-from-array accepts, so it round-trips into job-interview-update) plus basic metadata. Access is subject to the caller's row-level security. # List interview definitions Source: https://developer.jobmojito.com/api-reference/interviews/list-interview-definitions https://cool.jobmojito.com/functions/v1/openapi get /merchant-interview-list Paginated list of a merchant's interview definitions (the admin-portal interview list), scoped to your token's merchant (or a merchant_id override). Capped at 1000 records per page. # Register users for an interview Source: https://developer.jobmojito.com/api-reference/interviews/register-users-for-an-interview https://cool.jobmojito.com/functions/v1/openapi post /job-interview-register-users Registers (or updates) one or more candidate profiles for an interview and returns a one-time interview URL for each. Accepts a Supabase user JWT (merchant/admin) or the service key (merchant_id then required). # Set interview state Source: https://developer.jobmojito.com/api-reference/interviews/set-interview-state https://cool.jobmojito.com/functions/v1/openapi post /job-interview-set-state Changes the lifecycle status of an interview or position (draft, active, archived, preparing, completed, deleted) and/or manages its embed key. Provide at least one of `status` or `is_embedded`. # Update an interview or position Source: https://developer.jobmojito.com/api-reference/interviews/update-an-interview-or-position https://cool.jobmojito.com/functions/v1/openapi post /job-interview-update Updates the configuration of an existing interview (single-stage) or position (multi-stage). Only the fields present in the request body are written — everything else keeps its current value, and sending null clears a nullable field. The question list is only touched when you send `questions`; omit that field and the questions are left exactly as they are. When sent, it must be the complete list and is applied as a diff against what is stored (matched on external_id, then id, then identical content), so unchanged questions keep their existing records, edited ones are unlinked and re-created, and dropped ones are unlinked — and re-sending the array job-interview-get returned changes nothing. See the field description, and `questions_diff` in the response for what was decided. Questions of an interview that is already `active` can only be changed on interactive avatar templates, where re-publishing is instant; on the offline (pre-rendered video) templates set the interview back to `draft` first. The welcome / thank-you messages and the instructional-video screen are stored as steps rather than questions and are never changed here. `mojito_language_code` cannot be changed — the existing questions and rendered videos are in the original language — so create a new interview to change language. A multi-stage position only carries the shared identity fields (name, code, location, description, description_long, cover_image_url, department, salary, available_till, recruiter, status, visibility, hiring_for_company); sending an interview-only field for a position is a 422. # Upload a knowledge base document Source: https://developer.jobmojito.com/api-reference/knowledge-base/upload-a-knowledge-base-document https://cool.jobmojito.com/functions/v1/openapi post /knowledge-base-document-upload Uploads a document to a knowledge base store and queues it for processing. Accepts the file either as multipart/form-data (binary `file`) or as application/json (base64 `file`). # Generate an interview report Source: https://developer.jobmojito.com/api-reference/results/generate-an-interview-report https://cool.jobmojito.com/functions/v1/openapi post /job-interview-pdf Generates an interview result report as a PDF (returns a signed URL), raw HTML, or structured JSON. Provide either `interview_result_id` for a single result or `interview_result_ids` for a combined multi-result report. # Get daily event analytics Source: https://developer.jobmojito.com/api-reference/results/get-daily-event-analytics https://cool.jobmojito.com/functions/v1/openapi get /merchant-analytics Daily event-count time-series for a merchant over a date range (the admin-portal analytics events graph), scoped to your token's merchant (or a merchant_id override). Optionally drilled to a single interview. Capped at 1000 records per page. Note: only day/event combinations with a non-zero count are returned — any day/event pair absent from the response should be treated as a count of 0 by the caller. # Get interview result details with transcript Source: https://developer.jobmojito.com/api-reference/results/get-interview-result-details-with-transcript https://cool.jobmojito.com/functions/v1/openapi get /job-interview-details Returns an interview result with its full transcript and AI assessment. Optionally attaches signed recording URLs. # Get merchant credit usage Source: https://developer.jobmojito.com/api-reference/results/get-merchant-credit-usage https://cool.jobmojito.com/functions/v1/openapi get /merchant-analytics-credits-used Per-event credit-usage ledger for a merchant: every billable analytics event (interview, pre-screening, public avatar, simulation, …) that consumed credits, ordered most recent first. Scoped to your token's merchant, or a merchant_id override for admins / sub-merchant operators. The credits consumed by each event are in stats.credit_amount. Capped at 1000 records per page. # List interview results Source: https://developer.jobmojito.com/api-reference/results/list-interview-results https://cool.jobmojito.com/functions/v1/openapi get /merchant-result-list Paginated list of a merchant's interview results (the admin-portal results list), scoped to your token's merchant (or a merchant_id override). Capped at 1000 records per page. # Request another interview attempt Source: https://developer.jobmojito.com/api-reference/results/request-another-interview-attempt https://cool.jobmojito.com/functions/v1/openapi post /job-interview-result-request-another-attempt Marks a submitted (active + completed) interview result as unsuccessful so the candidate can retake it. Resets the result to draft and clears the recruiter decision. # Create or update a pre-screening position Source: https://developer.jobmojito.com/api-reference/resume-&-form-verification/create-or-update-a-pre-screening-position https://cool.jobmojito.com/functions/v1/openapi post /pre-screening-create Creates a new pre-screening position (and optionally activates it) or, when update_position_def_set_id is provided, updates an existing pre-screening position. Only merchant_owner, merchant, or admin users may call this. The screening behaviour is driven by two related arrays: - `assessment_rules` — the rules enforced on each candidate. Only rules with `type: "screening"` (plus `resume_match` / `resume_technical_experience`) actually pass/reject; other entries just declare collected fields. - `form_fields` — the candidate-facing form (standard field references and custom fields). For `form` and `resume_with_form` types, enable a standard field in BOTH arrays: add the rule to `assessment_rules` and a matching `{ id, mandatory }` entry to `form_fields`. `resume`-only positions use `assessment_rules` (resume_* rules) and ignore `form_fields`. Both arrays are replaced wholesale on update — send the complete array, not a delta. # Pre-screen a candidate from a binary resume upload Source: https://developer.jobmojito.com/api-reference/resume-&-form-verification/pre-screen-a-candidate-from-a-binary-resume-upload https://cool.jobmojito.com/functions/v1/openapi post /job-interview-pre-screening-api-resume-binary Accepts a multipart/form-data upload with candidate details and a binary resume file, converts the resume to text, runs AI pre-screening against the position, stores the result, and returns the decision. # Run pre-screening on a plaintext resume Source: https://developer.jobmojito.com/api-reference/resume-&-form-verification/run-pre-screening-on-a-plaintext-resume https://cool.jobmojito.com/functions/v1/openapi post /job-interview-pre-screening-api-resume-text Stores a plaintext candidate resume, runs AI pre-screening against the position definition, records the result and returns the AI recommendation and analysis. If the candidate is not in the pre-screening candidate_action state, an early `{ decision_status, message }` object is returned with HTTP 200. # Interview answers collected Source: https://developer.jobmojito.com/api-reference/webhooks/interview-answers-collected https://cool.jobmojito.com/functions/v1/openapi-webhooks webhook interview_collected Sent when the candidate finishes the interview and their raw answers are collected, before final AI scoring. # Interview failed Source: https://developer.jobmojito.com/api-reference/webhooks/interview-failed https://cool.jobmojito.com/functions/v1/openapi-webhooks webhook interview_failed Sent when an interview does not complete successfully — failed, abandoned, or terminated with no speech detected. The scoring and AI-analysis fields (score, ai_analysis*, ai_analysis_recruiter_score, recruiter decision, pdf_export_url) are typically empty/null for this event, since the interview did not complete. # Interview result submitted Source: https://developer.jobmojito.com/api-reference/webhooks/interview-result-submitted https://cool.jobmojito.com/functions/v1/openapi-webhooks webhook interview_result Sent when an interview or assessment is submitted by the candidate (the result is finalized and AI-scored). If the interview is configured to generate a PDF report, this event is delayed until the report is ready so `pdf_export_url` is included. Also re-sent when a result field changes afterwards (score, recruiter decision, AI analysis, or the PDF report URL). # Authentication Source: https://developer.jobmojito.com/authentication Authenticate JobMojito API requests with a bearer token in the Authorization header. Every JobMojito API request is authenticated with a **bearer token** sent in the `Authorization` header: ```http theme={null} Authorization: Bearer ``` The token identifies your merchant account (and the sub-merchants it can access). Requests without a valid token are rejected with **HTTP 401**. ```bash theme={null} curl https://cool.jobmojito.com/functions/v1/job-interview-get \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "interview_id": "00000000-0000-0000-0000-000000000000" }' ``` ## Getting a token Your API credentials are issued from the JobMojito admin. If you don't have them yet, contact your account manager or [**support@jobmojito.com**](mailto:support@jobmojito.com). ## Who the token represents The token is bound to a **merchant** account. Most endpoints automatically scope their work to that merchant - you don't pass a `merchant_id` yourself. A few service-level integrations may operate across merchants; in those cases the endpoint documents a required `merchant_id` field and returns **422** if it's missing. ## Authorization errors | Status | Meaning | | - | - | | `401` | Missing, malformed, or expired token. | | `403` | The token is valid but not allowed to perform this action (for example, a non-merchant user). | ## Keep your token secret Treat the token like a password: * Send it only over HTTPS (all JobMojito endpoints are HTTPS-only). * Store it in a secret manager / environment variable, never in client-side code or version control. * Rotate it if you suspect it has leaked. Next: see [How the API works](/how-the-api-works) for the base URL, request format and error model. # Overview Source: https://developer.jobmojito.com/cookbooks Task-oriented, end-to-end guides for the most common JobMojito workflows. These cookbooks walk through complete workflows — from creating an interview to reviewing a candidate's results — using the JobMojito API. Each recipe lists the exact endpoints, required fields, and a runnable example. Working through an AI assistant connected to the JobMojito MCP server? Each recipe also names the matching MCP tool (e.g. `create_interview`), so the same steps apply whether you call the REST API directly or via the MCP. ## Before you begin All requests share the same base URL and authentication. ```bash Base URL theme={null} https://cool.jobmojito.com/functions/v1 ``` ```bash Authentication theme={null} # Every request needs a Supabase JWT access token: Authorization: Bearer ``` Requests and responses are JSON unless noted. Calls act on behalf of the authenticated merchant user, so results respect that user's permissions. ## Recipes Generate an AI interview from a job position, from your own questions, in one shot for a specific candidate, or as a role-play persona — for coaching or as an interview-portal role-play. Read an interview's question list, change it, and send it back — without disturbing the questions you did not touch. Generate signed interview links or send branded email invitations. Score a résumé against a position before inviting them to interview. List results, read the full transcript and AI assessment, and export reports. Upload documents so interviews can ask grounded, knowledge-based questions. Organise the coaching portal into directories, place sessions and personas into them with tags, and author custom directory pages. The vocabulary for every field that renders as Markdown — headings, tables and code, plus chips, callouts, cards, columns and buttons. # Build a coaching catalogue Source: https://developer.jobmojito.com/cookbooks/build-a-coaching-catalogue Organise the coaching portal into directories, place coaching sessions and personas into them with tags, author custom directory pages, and offer each page in several languages. The **coaching catalogue** is what a learner browses on the coaching portal. It is a tree of **directories**; each directory is one page at `/catalogue/` on your coaching domain, and it can hold two things: * **Sub-directories** — listed explicitly in the directory's `tags_sub`. * **Coaching sessions** — *not* listed explicitly. A session appears because its own `tags` match the directory's `tags_interview_set_filter`. That indirection is the whole design: you tag a session once, and it shows up in every directory whose filter it satisfies. Nothing has to be re-linked when you add a session. Coaching-platform feature. Directories only render on a coaching portal; they have no effect on the recruiting/interview portal. ## The mental model ```text theme={null} merchant.catalogue_tag_start → the directory the catalogue opens on └── home (is_start_directory: true) ├── tags_sub: [sales-coaching, interview-prep] │ ├── sales-coaching │ │ ├── tags_interview_set_filter: ["sales-en"] │ │ ├── lists every active coaching/persona session tagged "sales-en" │ │ └── translations.sk: its Slovak version, with its own filter ["sales-sk"] │ └── interview-prep └── content_md: optional custom page that lays the above out by hand ``` Every directory can carry **several languages**. Its top-level fields are its default language (`mojito_language_code`); `translations` holds the others, each a page of its own. See [Several languages in one directory](#several-languages-in-one-directory). | Tool | What it is for | | - | - | | `list_catalogue_directories` (`GET /catalogue-tag-list`) | Find directories; walk the tree with `parent_tag`; spot the root with `is_start_directory`. | | `get_catalogue_directory` (`GET /catalogue-tag-get`) | One directory in full — its `content_md`, its sub-directories, and **the sessions it currently lists**. | | `create_catalogue_directory` (`POST /catalogue-tag-create`) | Create a directory, optionally nested under a `parent_tag`. | | `update_catalogue_directory` (`POST /catalogue-tag-update`) | Change a directory, its filter, its children, or its custom page. | | `update_interview` (`POST /job-interview-update`) | Set `tags` on an existing session so it lands in a directory. | ## How a session gets into a directory A session is listed when **all** of these are true: | Condition | Where it comes from | | - | - | | Its `tags` contain **every** tag in the directory's `tags_interview_set_filter` | `tags` on the session, `tags_interview_set_filter` on the directory | | Its `type` is `coaching` or `persona` | `type` on create; `persona-create` with `portal: "coaching"` (the default) | | Its `status` is `active` | `status` on create, or `set_interview_state` | | Its `visibility` is `public` or `merchant_public` | `visibility` on create/update | The tag filter is an **AND, not an OR**. A directory filtering on `["sales", "objection-handling"]` lists only sessions carrying *both* tags. A session may carry extra tags — that is what lets one session appear in several directories. Two edge cases are worth knowing: * `tags_interview_set_filter: []` (empty array) matches **every** tagged session — a useful "everything" directory, and an easy accident. * `tags_interview_set_filter: null` matches **nothing**. A brand-new directory created without the field lists no sessions until you set it. ## Walkthrough Start from the root and walk down, rather than guessing ids. ```bash theme={null} # Everything you can see, newest ids first — is_start_directory marks the root curl "https://cool.jobmojito.com/functions/v1/catalogue-tag-list?mojito_language_code=en" \ -H "Authorization: Bearer $SUPABASE_JWT" # Just the children of one directory, in the order they are rendered curl "https://cool.jobmojito.com/functions/v1/catalogue-tag-list?parent_tag=home-employee-en" \ -H "Authorization: Bearer $SUPABASE_JWT" ``` `include_public=true` (the default) also returns the platform-wide directories shared across merchants. Set it to `false` to see only your own. `mojito_language_code` matches a directory's default language **or** any of its language versions; each row's `languages` lists them, default first. The `id` is the URL segment and the value other directories reference in their `tags_sub`. Use lowercase letters, digits and single `-`/`_` separators. A directory that will hold several languages is better named without a language suffix (`sales-coaching`, not `sales-coaching-en`): the same address serves every language. The API cannot change an id afterwards; a merchant can, in the admin catalogue editor (see [Gotchas](#gotchas)). ```bash theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/catalogue-tag-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "id": "sales-coaching-en", "name": "Sales coaching", "description": "Practice discovery, objection handling and closing.", "mojito_language_code": "en", "visibility": "merchant_public", "status": "active", "parent_tag": "home-employee-en", "tags_interview_set_filter": ["sales"] }' ``` `parent_tag` appends the new id to that directory's `tags_sub`, so the page is reachable immediately. Omit it and the directory exists but is only reachable by direct link until you add it to a parent. The response returns `catalogue_url` — the live page — when your merchant has a coaching domain configured. A **new** session takes its tags on create. All three creation endpoints accept `tags`: ```bash Coaching session (AI-generated questions) theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Handling price objections", "location": "remote", "interview_template_id": "TEMPLATE_UUID", "mojito_language_code": "en", "status": "active", "type": "coaching", "visibility": "merchant_public", "tags": ["sales", "objection-handling"] }' ``` ```bash Role-play persona theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/persona-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Objection-handling role-play", "interview_template_id": "TEMPLATE_UUID", "mojito_language_code": "en", "status": "active", "visibility": "merchant_public", "portal": "coaching", "persona_role_avatar": "is to act as a skeptical procurement manager", "persona_role_user": "is to be a sales rep booking a follow-up demo", "tags": ["sales", "objection-handling"] }' ``` An **existing** session is re-tagged with `update_interview`. `tags` is replaced wholesale, so read the current value first and send the extended list: ```bash theme={null} # 1. read the current tags curl "https://cool.jobmojito.com/functions/v1/job-interview-get?position_id=SESSION_UUID" \ -H "Authorization: Bearer $SUPABASE_JWT" # 2. send the full list, not just the addition curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-update \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "position_id": "SESSION_UUID", "tags": ["sales", "objection-handling"] }' ``` `persona-create` defaults to `portal: "coaching"`, which is the variant that belongs in a catalogue. A persona created with `portal: "interview"` is stored as `persona_interview` and is **never** listed in the catalogue — it goes through the recruiting invitation flow instead. See [Create an interview](/cookbooks/create-an-interview#two-variants). Do not assume the tags matched. `get_catalogue_directory` returns the sessions the directory actually lists, applying exactly the rules the live portal applies: ```bash theme={null} curl "https://cool.jobmojito.com/functions/v1/catalogue-tag-get?id=sales-coaching-en" \ -H "Authorization: Bearer $SUPABASE_JWT" ``` ```json theme={null} { "id": "sales-coaching-en", "tags_interview_set_filter": ["sales"], "matched_sessions": [ { "id": "9c1b…e4f2", "name": "Handling price objections", "type": "coaching", "status": "active", "visibility": "merchant_public", "tags": ["sales", "objection-handling"] } ], "sub_directories": [], "catalogue_url": "https://coaching.example.com/catalogue/sales-coaching-en" } ``` An empty `matched_sessions` almost always means one of four things: the session is still `draft`, its visibility is `merchant_invite`/`merchant_unlisted`, its `type` is not `coaching`/`persona`, or its tags are missing one of the filter's tags. By default a directory renders as a plain grid: sub-directories, then sessions. Set `content_md` and your Markdown takes over the layout instead. ```bash theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/catalogue-tag-update \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "id": "sales-coaching-en", "content_md": "## Sales coaching\n\nStart with discovery, then work up to objections.\n\n[plan-progress]\n\n### Practise a conversation\n\n[sessions:filter=objection-handling,limit=6]\n\n### Go deeper\n\n[directory:sales-coaching-closing-en]\n" }' ``` Directives must sit **alone on their own line** — one on a line with other text renders as ordinary text: | Directive | Renders | | - | - | | `[plan-progress]` | The learner's coaching-plan progress. | | `[directory:]` | A card for one sub-directory. | | `[session:]` | A card for one session. | | `[sessions]` | Every session in this directory. | | `[sessions:]` | Sessions matching a term. | | `[sessions:filter=,limit=]` | A filtered, capped list. | `content_md` is replaced wholesale. Read the current page with `get_catalogue_directory` and send the edited whole — not a fragment. Sending `null` removes the custom page and restores the default grid. A directive can only show sessions the directory already lists. If `[sessions:filter=…]` renders nothing, check `matched_sessions` first — the filter narrows that set, it does not widen it. `content_md` also renders chips, callouts, cards, columns and buttons on top of standard Markdown — see [Format content with Markdown](/cookbooks/format-content-with-markdown) for the full vocabulary. ## Several languages in one directory One directory can be offered in several languages. Each language is a **version** with its own `name`, `description`, `content_md`, cover, sub-directories (`tags_sub`) and session filter (`tags_interview_set_filter`) — a page of its own, at the same address. The directory's top-level fields are its **default** language (`mojito_language_code`); `translations` maps every other language to its version. A learner sees the version in the language they picked in the catalogue's language switch (remembered across pages), else their portal language; when the directory lacks it, English, then the default. The switch appears only on directories with more than one language. A coaching session itself has one language (its `mojito_language_code`). A "multilingual session" is therefore **one session per language**, gathered into one directory whose versions each list their own language's sessions. The filter is tags-only, so give each language's sessions their own tag, e.g. `sales-en` and `sales-sk`, and set each session's `mojito_language_code`. ```bash theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Obchodné námietky", "type": "coaching", "mojito_language_code": "sk", "interview_template_id": "TEMPLATE_UUID", "location": "remote", "status": "active", "visibility": "merchant_public", "tags": ["sales-sk"] }' ``` Top-level fields are the default language; each `translations` entry is another language's version. `name` is required in every version; every other field is optional. A version without `tags_interview_set_filter` lists no sessions. ```bash theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/catalogue-tag-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "id": "sales-coaching", "parent_tag": "home", "mojito_language_code": "en", "name": "Sales coaching", "tags_interview_set_filter": ["sales-en"], "content_md": "## Sales coaching\n\n[sessions]\n", "translations": { "sk": { "name": "Obchodný koučing", "tags_interview_set_filter": ["sales-sk"], "content_md": "## Obchodný koučing\n\n[sessions]\n" } } }' ``` `translations` on `update_catalogue_directory` is a **patch per language** — unlike `content_md` or `tags_sub`, languages you do not send are kept. An object replaces that language's version; `null` removes it. ```bash theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/catalogue-tag-update \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "id": "sales-coaching", "translations": { "cs": { "name": "Obchodní koučink", "tags_interview_set_filter": ["sales-cs"] }, "de": null } }' ``` A version is replaced whole: to change one field of it, read it from `get_catalogue_directory` → `translations.` and send the edited version. `translations: null` removes every version but the default. `get_catalogue_directory` returns `languages` (default first) and `translations`. Its `matched_sessions` and `sub_directories` describe the default language; pass `mojito_language_code` to see them for another version: ```bash theme={null} curl "https://cool.jobmojito.com/functions/v1/catalogue-tag-get?id=sales-coaching&mojito_language_code=sk" \ -H "Authorization: Bearer $SUPABASE_JWT" ``` `viewed_language` in the response says which version the lists belong to. A language the directory does not have answers `422`, so a missing translation cannot pass for a working one. **Changing the default language.** Send `mojito_language_code` on `update_catalogue_directory`. If the directory already has that language, its version becomes the default and the old default stays as a version. If it does not, the default version is simply relabelled. Top-level fields sent in the same request always describe the default *after* the change. **Sub-directories and cards.** A directory id is the same in every language, so `tags_sub` and `[directory:]` cards name the same ids in each version; each card renders in the learner's language. ## Coaching plans `coach_plan` marks a directory (or a session) as a stage of a guided plan — `demo`, `screening`, `2nd`, `3rd`, `closing`, `job-specific`, `other`. Learners then see their progress through those stages wherever a page carries the `[plan-progress]` directive. Leave it null for anything that is not part of a plan. ## Gotchas | Gotcha | What to do | | - | - | | Tag filter is an AND | Every tag in `tags_interview_set_filter` must be on the session. Filter on one tag unless you deliberately want the intersection. | | `tags`, `tags_sub`, `tags_interview_set_filter` and `content_md` are all **replace, not merge** | Read the current value first, then send the full new value. | | An empty `tags_interview_set_filter` matches everything tagged | Set the filter deliberately; `[]` is not "nothing". | | The directory `id` cannot be changed through the API | To rename the *page*, change `name`. To change the address, a merchant uses **Change address** in the admin catalogue editor: every `tags_sub`, `[directory:…]` card, `/catalogue/…` link, start directory and coaching-plan step moves with it, and the old address keeps redirecting. `get_catalogue_directory` accepts an old id and returns the directory under its current `id`. | | `translations` is a patch per language; everything else is replace | Languages you do not send are kept, `null` removes one. Each version you send replaces that whole version. | | A version without `tags_interview_set_filter` lists no sessions | Give every version its own filter — usually its language's tag (`sales-sk`). | | A row has `merged_into` set | It was folded into another directory as one of its languages; its address redirects there. Edit the directory `merged_into` names. | | A session in `draft` never appears | Publish it with `set_interview_state` (or `status: "active"` on `update_interview`). | | `merchant_invite` / `merchant_unlisted` sessions never appear | The catalogue only lists `public` and `merchant_public`. | | An id in `tags_sub` that no longer resolves | `get_catalogue_directory` returns it in `tags_sub` but not in `sub_directories` — compare the two to find broken links. | | A session's `type_credit` decides which consumer credit bucket a learner spends | Set it on `update_interview` if the default (`interview_coach_manager`) is wrong for the session. | ## Next steps Create the coaching sessions and role-play personas the catalogue lists. Read transcripts and feedback once learners complete a session. # Create an interview Source: https://developer.jobmojito.com/cookbooks/create-an-interview Generate an AI interview from a job position, from your own questions, or in one shot for a candidate. There are four ways to create an interview, depending on how much you want JobMojito to generate for you: | You provide | Use | MCP tool | | - | - | - | | A job description → AI generates questions | `POST /job-interview-create` | `create_interview` | | Your own list of questions | `POST /job-interview-create-from-array` | `create_interview_from_questions` | | A candidate + position, get a ready link back | `POST /job-interview-create-for-candidate-with-token` | — (HTTP only, not an MCP tool) | | A role-play persona (avatar plays a role, no Q\&A) — two variants: coaching persona, or an interview-portal interview role-play | `POST /persona-create` | `create_persona` | You'll need an `interview_template_id`. List your available templates with `GET /merchant-avatar-list` (MCP tool `list_avatars`) and pick one. ## The template picks the interview modality The `interview_template_id` is not just cosmetic — **the template's `type` decides whether the candidate gets a voice-only or an avatar experience.** Choose it deliberately; there is no separate "voice or avatar" flag on the create call. | Template `type` (from `list_avatars`) | Candidate experience | | - | - | | `interactive_elevenlabs` | **Voice-only** — realtime conversation, no video avatar. | | `interactive_heygen` | **Realtime interactive avatar** — a talking video avatar. | | `offline_heygen` | **Non-interactive avatar** — pre-recorded avatar video (no live back-and-forth). | `offline_elai` and `offline_synthesia` are **legacy** integrations. They may still appear in `list_avatars` for older accounts, but they **cannot be used to create new interviews** — only `offline_heygen` is supported for the pre-recorded modality. Filter directly to the modality you want: `GET /merchant-avatar-list?type=interactive_elevenlabs` for voice-only, or `?type=interactive_heygen` for a realtime avatar. The item's `id` is the `interview_template_id` you pass below. Some options only work on avatar templates. On a **voice-only** (`interactive_elevenlabs`) template, `early_stop` scoring and `instructional_video` are unsupported and the create call returns **422**. Pick an `interactive_heygen` template if you need those. To confirm the modality of an interview you've already created, read it back with `get_interview_definition` (`POST /job-interview-get`) — the response includes `interview_template_type` and a convenience `is_voice_only` boolean. ## Option A — from a job position (AI-generated questions) JobMojito writes the description, questions, and candidate expectations for you. Call `GET /merchant-avatar-list` and note an `interview_template_id`. Send the position details. The required fields are `name`, `location`, `interview_template_id`, `mojito_language_code`, `status`, `type`, and `visibility`. ```bash cURL theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Senior Backend Engineer", "location": "Remote (EU)", "interview_template_id": "TEMPLATE_UUID", "mojito_language_code": "en", "status": "active", "type": "interview", "visibility": "merchant_invite", "interview_type": "pre-screening", "seniority_level": "senior", "interview_length": 8, "description": "Backend role focused on Python, async APIs, and AWS.", "candidate_expectations": "5+ yrs Python, REST/async, cloud, SQL." }' ``` The defaults are generic. In the **same create call**, set these fields to tailor the interview — they are easy to forget: * `welcome_message` — the custom message shown to the candidate **before** they start. Set it to greet the candidate and set expectations. * `thank_you_message` — the custom message shown **after** they finish. * `custom_scoring` — an object of result-scoring overrides (your custom rubric). It is merged over the platform defaults and any template overrides, so you only need to include what you want to change. Omit it to use the template/default scoring. ```json theme={null} { "welcome_message": "Welcome! This is a 15-minute screen for the Senior Backend role. Answer out loud; there are no trick questions.", "thank_you_message": "Thanks for your time — our team will review your responses and be in touch within a week.", "custom_scoring": { "criteria": [ { "name": "System design", "weight": 40 }, { "name": "Coding fundamentals", "weight": 40 }, { "name": "Communication", "weight": 20 } ] } } ``` `custom_scoring` is a free-form overrides object — its exact shape follows your account's scoring model. Fetch an existing interview with `get_interview_definition` to see the current scoring structure before overriding it. The response returns `interview_def_set_id` — keep it; you'll use it to invite candidates and review results. Note it is passed back to other endpoints under different field names (`position_id`, `interview_id`) — see [Identifiers & admin links](/mcp/identifiers). ```json theme={null} { "interview_def_set_id": "9c1b…e4f2" } ``` ### Key fields | Field | Required | Notes | | - | - | - | | `name`, `location` | ✓ | Position title and location. **Plain UTF-8 text** — displayed to the candidate as-is, so no Markdown or HTML (any `#`, `*`, or `` show up literally). | | `interview_template_id` | ✓ | Template id from `list_avatars`. **Its `type` sets the modality** (voice-only vs avatar) — see [The template picks the interview modality](#the-template-picks-the-interview-modality). | | `mojito_language_code` | ✓ | Platform language code (e.g. `en`). | | `status` | ✓ | `draft`, `active`, `archived`, or `deleted`. | | `type` | ✓ | Product type, e.g. `interview`. | | `visibility` | ✓ | Who can access it, e.g. `merchant_invite`, `merchant_public`. | | `description` | – | Short, **two-sentence** job description **shown to the candidate**, as **plain UTF-8 text** — no Markdown or HTML (unlike `description_long`; markup here renders literally). Provide it to use it as-is, or leave it null/blank and it is **AI-generated** from the position name (and any other context). Also fed into question generation. | | `description_long` | – | Full job description in **Markdown** (Job Purpose, Responsibilities, Required & Preferred Qualifications). This is the **only** create field that accepts Markdown; `name` and `description` are plain text. Provide it to use it as-is, or leave it null/blank and it is **AI-generated**. | | `candidate_expectations` | – | Free-text candidate expectations, fed into AI question generation. | | `interview_length` | – | Number of questions to generate (1-40), **capped by `max_duration`** — see [How many questions fit](#how-many-questions-fit). Omit it and the AI picks 5-8. | | `welcome_message`, `thank_you_message` | – | Custom candidate-facing messages — **set these; the defaults are generic.** | | `custom_scoring` | – | Custom scoring-rubric overrides (object), merged over defaults. | | `knowledge_base_store_id` | – | Ground questions in a [knowledge base](/cookbooks/manage-knowledge-base). | ### How many questions fit `interview_length` is capped by `max_duration`, which allows **one question per 2 minutes** — enough for the question, the answer, and automatic follow-ups. | `max_duration` | Interview length | Max questions | | - | - | - | | `1200` (default) | 20 min | 10 | | `1800` | 30 min | 15 | | `2700` | 45 min | 22 | | `3600` | 60 min | 30 | | `4800` | 80 min | 40 | Asking for more than the cap is not an error — you get the cap. The create response returns `questions_generated` with the real count, so check it if the number matters to you. ### Additional configuration These optional fields fine-tune the position, the conversation, and what candidates see. They apply to **`job-interview-create` and `job-interview-create-from-array`** (and, where noted, `job-interview-create-for-candidate-with-token`). All are optional — omit any you don't need. | Field | Type | Notes | | - | - | - | | `interview_department` | string | Department the position belongs to. | | `interview_salary` | string | Salary range shown for the position. | | `interview_available_till` | ISO date/time | When the interview stops being available to candidates. | | `interview_conversation_speed` | `slower` \| `normal` \| `faster` | Speaking pace of the AI avatar. | | `max_followups` | int `0`–`999` | Max AI follow-up questions. `0` disables follow-ups; omit for the template default. | | `questions_random_subset` | number `0.01`–`0.9` | Ask only a random fraction of questions (e.g. `0.5` = 50%). | | `required_pronunciation` | boolean | Require pronunciation assessment (restricts to pronunciation-capable languages). | | `result_enable_edit_transcript` | boolean | Allow editing the transcript on the result view (default `true`). | | `tags` | string\[] | Free-form tags stored on the interview. | | `recruiter_profile_id` | uuid | Recruiter (merchant profile) that owns the interview. | | `hiring_for_company` | object | Who the role is really for (see below). Omit (or `name` null/blank) for yourself, `{ "name": "undisclosed" }` for an unnamed client, or `{ "name": "" }` plus optional `description`/`location`/`sector`/`company_size` for a named client. | | `pdf_export_auto_config` | object | Auto-generate a candidate PDF report when the interview completes (see below). | `hiring_for_company` describes the end employer so the AI agent can answer candidate questions accurately. All fields are optional; `name` carries the original meaning: ```json theme={null} { "hiring_for_company": { "name": "JobMojito", "description": "World leading AI assessment provider.", "location": "Estonia", "sector": "AI Interview", "company_size": "100-200" } } ``` * Omit the field entirely, or send `name` null/blank → hiring for yourself (no external-client note). * `{ "name": "undisclosed" }` → external client whose name is withheld from the candidate. * `{ "name": "" }` → named external client; `description`, `location`, `sector`, and `company_size` are surfaced to the agent as background context. `pdf_export_auto_config` mirrors the report options — every key is optional: ```json theme={null} { "pdf_export_auto_config": { "mojito_language_code": "en", "contact_details": true, "ai_recruiter_assessment": true, "ai_scoring_rubric": false, "analytics": true, "transcript": true, "files": true, "answer_recording": false, "session_recording": false, "group_by_question": false } } ``` On `job-interview-create-for-candidate-with-token` these same fields are honoured (it creates the interview through the multistage flow). They require the `interview_create_multi_stage` migration to be deployed. ## Option B — from your own questions Supply an ordered `questions` array. Same required interview fields as Option A, plus a `description`. ```bash cURL theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-create-from-array \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Support Specialist", "location": "Remote", "interview_template_id": "TEMPLATE_UUID", "mojito_language_code": "en", "description": "Customer support screening", "status": "active", "type": "interview", "visibility": "merchant_invite", "questions": [ { "question": "Tell me about a time you de-escalated an angry customer.", "duration": 120 }, { "question": "How do you prioritize a full support queue?", "duration": 90 }, { "question": "Rate your written English.", "is_multiple_choice": true, "question_alternatives": ["Native", "Fluent", "Intermediate"] } ] }' ``` Each `questions` item requires `question` (the text). Useful per-question flags: `duration` (seconds), `is_without_scoring`, `is_multiple_choice`, `question_alternatives`, `is_conditional` + `conditional_question_main_id` (for follow-ups), and `knowledge_base_id` (ask grounded questions — see [Manage a knowledge base](/cookbooks/manage-knowledge-base)). The personalization fields from Option A — `welcome_message`, `thank_you_message`, and `custom_scoring` — plus everything in [Additional configuration](#additional-configuration) apply here too. Set them in the same call so the candidate experience and rubric aren't left on the generic defaults. You may also pass `candidate_expectations_json` — a structured expectations object bucketed by requirement level (`{ "weak": [...], "moderate": [...], "strong": [...] }`). Omit it and JobMojito auto-generates one for `type: "interview"`. Returns `interview_def_set_id`. This same `questions` format comes back from `GET /job-interview-get` and is accepted by `POST /job-interview-update`, so you can change the questions of an interview later without recreating it — see [Edit interview questions](/cookbooks/edit-interview-questions). ## Option C — one call per candidate (returns a link) Create the position (or reuse one), enrol a candidate, run pre-screening, and get a tokenised interview URL back — all in one request. ```bash cURL theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-create-for-candidate-with-token \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "candidate_name": "Peter Parker", "candidate_email": "peter@parker.com", "candidate_country_code": "US", "candidate_resume": "Full plaintext résumé here…", "position_name": "Senior Backend Engineer", "position_location": "Remote (EU)", "position_country_code": "DE", "mojito_language_code": "en" }' ``` If you already have a position, pass `position_def_set_id` instead of the `position_*` fields. The response includes `status` (e.g. `ai_accept`, `recruiter_action`, `ai_reject`), `interview_url`, and the created `interview_def_set_id` / `position_def_set_id`. Need to publish, archive, or unpublish later? Use `POST /job-interview-set-state` (`set_interview_state`) with the `position_id` and a `status` of `active`, `draft`, `archived`, or `deleted`. Retrieve a definition — questions included — with `POST /job-interview-get` (`get_interview_definition`), and change its settings or questions with `POST /job-interview-update` (`update_interview`). ## Option D — a role-play persona (no Q\&A) A **persona** is a different product: the AI avatar plays a defined role in a free-form conversation instead of asking a scored list of questions. Use it for sales role-plays, mock-customer practice, or any "act as X" scenario. There is **no AI question generation** — the role fields you supply *are* the configuration. The session flows as: a welcome message → the avatar's **opening line** → the candidate replies and the role-play begins → a closing message. The `opening_line` is the literal first thing the avatar says; it's the only scripted line, so set it in character (it defaults to a bare "Hello" if you omit it). ### Two variants `POST /persona-create` creates one of two variants, chosen with the optional `portal` parameter. Both run the identical session mechanics — free-form role-play, no question list, the same `persona_*` and `opening_line` fields, and the same persona scoring — but they live on different portals and are billed differently. | | **Coaching persona** | **Interview role-play** | | - | - | - | | `portal` | `coaching` (the default) | `interview` | | Portal | Coaching portal only | Recruiter / interview portal | | Who takes it | Mentees on the coaching platform | Candidates, **invited through the normal invitation flow** | | Results | Visible to the coaching platform | Visible to **recruiters**, alongside regular interview results | | Billing | Consumer coaching credits | **Merchant credits**, using the standard interview cost formula | | Attempt limit | Unlimited | `interview_attempts` (default `3`) | | `type` in API responses | `persona` | `persona_interview` | `portal` defaults to `coaching`, so **existing callers are unaffected** — a `persona-create` call that doesn't mention `portal` behaves exactly as it did before. The **coaching persona** is otherwise unchanged: coaching portal only, billed against consumer coaching credits, with no attempt limit. The **interview role-play** (`portal: "interview"`) behaves like a persona in the session but like an interview commercially and operationally: * **Recruiters see the results.** Interview role-plays are scored on the persona scoring path — you get per-answer and session-level feedback, the same as a coaching persona — and completed sessions appear in the recruiter results list alongside regular interviews. See [Review results](/cookbooks/review-results). * **Candidates are invited normally.** Use the standard invitation flow (`POST /job-interview-register-users`) to generate links or send email invites — see [Invite candidates](/cookbooks/invite-candidates). * **It draws on merchant credits.** Cost is the standard interview formula — base format cost × duration multiplier, plus any recording add-ons — not consumer coaching credits. Each **completed** session is billed, so a candidate who uses every attempt costs a multiple of the single-session price. See [How credits work](/how-credits-work). * **Attempts are capped.** `interview_attempts` limits how many times a candidate may take the role-play; it defaults to **3**. The cap is not a hard ceiling. A recruiter can always grant one more go with `POST /job-interview-result-request-another-attempt` (`request_another_interview_attempt`) — a released attempt no longer counts against the cap. The first example creates a **coaching persona**, the second the **interview-portal interview role-play**. The role fields are identical for either variant. ```bash Coaching persona theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/persona-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Objection-handling role-play", "interview_template_id": "TEMPLATE_UUID", "mojito_language_code": "en", "status": "active", "visibility": "merchant_invite", "persona_role_avatar": "is to act as a busy procurement manager who is skeptical about switching vendors", "persona_role_user": "is to be a sales rep trying to book a follow-up demo", "persona_avatar_who_is": "Dana Reyes, procurement lead at a mid-size manufacturer. Runs a tight ship and is proud of it. She wants to stop firefighting supplier problems, and she is quietly worried that switching vendors mid-year will be blamed on her if it goes wrong. She will not engage with a pitch until someone acknowledges how much of her week the current vendor already costs her. She does not know that migration is handled by the supplier, not her team.", "persona_avatar_knowledge": "Current contract renews in 3 months. Budget is frozen until Q1. Two missed deliveries in the last quarter, on 14 March and 2 May. Her director, Tom, signs anything over 40k. She will raise price first, then switching risk. She would only mention that she is covering a colleague on parental leave, and is stretched thin, once the conversation feels genuinely useful to her.", "persona_avatar_progress": "Mode: turning point\nInitial hold: will not discuss switching while the conversation is about price.\nUnlocks when: the rep asks about the missed deliveries or the cost of managing the current vendor, rather than answering the price objection.\nAfter unlock: openly discusses the renewal timeline and will agree to a follow-up with Tom.\nHard limits: never commits to a contract on this call, and never accepts a discount as the reason to switch.\nNo goalposts: once unlocked, stays unlocked — no new objection is invented.\nGates: does not mention covering for a colleague on parental leave until after unlock.", "persona_avatar_end_conditions": "End once progress is complete — the follow-up with Tom is agreed — or once the conversation has clearly broken down.", "opening_line": "Look, I have about two minutes. We are pretty happy with our current vendor, so what is it you actually want?", "candidate_expectations": "Should uncover the renewal timeline and handle the price objection." }' ``` ```bash Interview role-play (interview portal) theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/persona-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Objection-handling role-play", "portal": "interview", "interview_attempts": 3, "interview_template_id": "TEMPLATE_UUID", "mojito_language_code": "en", "status": "active", "visibility": "merchant_invite", "persona_role_avatar": "is to act as a busy procurement manager who is skeptical about switching vendors", "persona_role_user": "is to be a sales rep trying to book a follow-up demo", "persona_avatar_who_is": "Dana Reyes, procurement lead at a mid-size manufacturer. Runs a tight ship and is proud of it. She wants to stop firefighting supplier problems, and she is quietly worried that switching vendors mid-year will be blamed on her if it goes wrong. She will not engage with a pitch until someone acknowledges how much of her week the current vendor already costs her. She does not know that migration is handled by the supplier, not her team.", "persona_avatar_knowledge": "Current contract renews in 3 months. Budget is frozen until Q1. Two missed deliveries in the last quarter, on 14 March and 2 May. Her director, Tom, signs anything over 40k. She will raise price first, then switching risk. She would only mention that she is covering a colleague on parental leave, and is stretched thin, once the conversation feels genuinely useful to her.", "persona_avatar_progress": "Mode: turning point\nInitial hold: will not discuss switching while the conversation is about price.\nUnlocks when: the rep asks about the missed deliveries or the cost of managing the current vendor, rather than answering the price objection.\nAfter unlock: openly discusses the renewal timeline and will agree to a follow-up with Tom.\nHard limits: never commits to a contract on this call, and never accepts a discount as the reason to switch.\nNo goalposts: once unlocked, stays unlocked — no new objection is invented.\nGates: does not mention covering for a colleague on parental leave until after unlock.", "persona_avatar_end_conditions": "End once progress is complete — the follow-up with Tom is agreed — or once the conversation has clearly broken down.", "opening_line": "Look, I have about two minutes. We are pretty happy with our current vendor, so what is it you actually want?", "candidate_expectations": "Should uncover the renewal timeline and handle the price objection." }' ``` Returns `interview_def_set_id` (and `embed_id` / `embed_signing_key` when `is_embedded` is true). ### Key fields | Field | Required | Notes | | - | - | - | | `name` | ✓ | Persona / session name. **Plain UTF-8 text** — shown to the candidate as-is, so no Markdown or HTML. | | `interview_template_id` | ✓ | Avatar template id from `list_avatars`. Its `type` sets the modality (voice vs avatar). | | `mojito_language_code` | ✓ | Platform language code (e.g. `en`). | | `status` | ✓ | `draft` or `active`. | | `visibility` | ✓ | Who can access it, e.g. `merchant_invite`. | | `description` | – | Short persona description shown to the candidate on the pre-session poster. **Candidate-visible — keep to max 2 sentences, plain UTF-8 text (no Markdown or HTML).** Never AI-generated (personas run no generation); omit it and the poster shows no description. | | `persona_role_avatar` | ✓ | The role the **avatar** plays. **Candidate-visible** ("Role of the agent" on the poster) — keep to max 2 sentences. | | `persona_role_user` | ✓ | The role the **candidate** plays. **Candidate-visible** ("Your role" on the poster) — keep to max 2 sentences. | | `opening_line` | – | The avatar's **first spoken line** (the literal words it says to open the scene, in character). Omit and it defaults to a generic "Hello" — set it for anything other than a neutral greeting. | | `persona_avatar_who_is` | – | Who the avatar represents: name, role, context and personality, woven together with what drives them underneath — motive, fear, what they refuse to move past until it is heard, and what they only learn when told. One continuous description of a person; **no labelled subsections**. | | `persona_avatar_knowledge` | – | The private facts the avatar may use: numbers, dates, names, the objections it will raise. Personal details it should only share after the conversation has progressed go here too, with the timing written **inline** rather than as a labelled subsection. Those personal details must never be something the candidate has to extract in order to reach the goal. | | `persona_avatar_progress` | – | **How the conversation is allowed to move forward.** Plain text whose first line is either `Mode: turning point` (a scene about resistance) or `Mode: steps` (a conversation run to a protocol), followed by that mode's labelled lines — one per line. See [Progression](#progression). Omit it and the avatar has no arc: it tends to either concede on the first polite question or never concede at all. | | `persona_avatar_end_conditions` | – | When the avatar should end the session. Write it to **refer back to** `persona_avatar_progress` rather than restate it: wrap up once progress is complete (turning point unlocked and a plan accepted, or the `Done when` state reached), or once the conversation has clearly broken down. | | `candidate_expectations` | – | What the person must achieve, and what a strong performance looks like (≤ 2100 chars). Technically optional, but this is the yardstick the session is scored against — it is passed to the grader as the goal of the session. Omit it and the scoring has nothing to measure the transcript against, so set it on anything you intend to assess. | | `portal` | – | **Decide this first.** `interview` for anything that assesses or screens candidates (recruiter-visible, invited, merchant-billed, attempt-capped). `coaching` is the **default when omitted** and creates the coaching-portal practice persona instead — so pass `interview` explicitly for recruiting use cases. See [Two variants](#two-variants). | | `interview_attempts` | – | Allowed candidate attempts (1-20), default `3` — the same field and behaviour as on `POST /job-interview-create`. **Only meaningful when `portal` is `interview`**; coaching personas are unlimited. | | `welcome_message`, `thank_you_message` | – | Candidate-facing messages spoken before the role-play starts and shown after it ends — **set these; the defaults are generic.** `welcome_message` sets the scene; the avatar's first in-character line is `opening_line`, which follows it. | Common optional fields also apply: `code`, `cover_image_url`, `interview_location`, `max_duration` (seconds, default `1200`), `recording`, `recording_full_session`, `result_view`, `interview_conversation_speed`, `candidate_video_introduction`, `tags`, `recruiter_profile_id`, `merchant_id`, and `is_embedded`. On the **interview role-play** variant (`portal: "interview"`), `max_duration`, `recording`, and `recording_full_session` drive the merchant-credit cost exactly as they do on a standard interview. ### Progression `persona_avatar_progress` is what stops a role-play from being either trivially easy or unwinnable. It is one plain-text value: the first line picks the mode, and the following lines are that mode's labels, one per line. **Use one mode, not both.** Pick **turning point** for a scene built on resistance the candidate has to work through, and **steps** for a difficult conversation the candidate is expected to run to a protocol. ```text Mode: turning point theme={null} Mode: turning point Initial hold: what the avatar will not budge on at the start. Unlocks when: the specific, observable thing the candidate must do. After unlock: how the avatar behaves once it happens. Hard limits: what it never agrees to, regardless. No goalposts: once unlocked it stays unlocked; no new obstacle is invented. Gates: which later personal details stay unsaid until after unlock. ``` ```text Mode: steps theme={null} Mode: steps Framework: the protocol being assessed. Steps: the ordered steps the candidate is expected to take. Done when: the observable state that ends the scene. Hard limits: what the avatar never agrees to, regardless. Gates: which later personal details stay unsaid until progress is made. ``` Two rules make the difference between a scenario that assesses something and one that does not: * **`Unlocks when` / `Done when` must be observable.** "The rep asks about the missed deliveries" is something an assessor can point at in the transcript. "The rep builds rapport" is not, and the avatar will apply it inconsistently. * **No moving goalposts.** Once the candidate has earned progress, the avatar must not invent a fresh obstacle. Without the `No goalposts` line, a role-play that is supposed to be winnable often is not. The `Gates` line is the link back to `persona_avatar_knowledge`: anything listed there should be one of the later personal details you wrote in that field, and none of them should be required to reach the goal. The same progression is also fed to the grader, so it scores whether the candidate actually did the thing that was meant to unlock progress — not whether the avatar happened to soften. ## Next steps Turn your new interview into links or email invitations. Read transcripts, scores, and export reports once candidates finish. # Edit interview questions Source: https://developer.jobmojito.com/cookbooks/edit-interview-questions Read an interview's question list, change it, and send it back — without disturbing the questions you did not touch. Questions live on an interview as an **ordered list**, and you edit them by reading that list, changing the array, and sending the whole thing back. | Step | Endpoint | MCP tool | | - | - | - | | Read the current questions | `GET /job-interview-get` | `get_interview_definition` | | Send back the list you want | `POST /job-interview-update` | `update_interview` | Both speak the **same question format** that [`POST /job-interview-create-from-array`](/cookbooks/create-an-interview#option-b-from-your-own-questions) accepts, so a definition read from one endpoint can be edited and written back to the other, or used to seed a copy of the interview. `questions` is **optional** on `job-interview-update`. Omit it and the question list is left completely alone — the endpoint only touches questions when you send the array. Everything else on the endpoint (name, scoring, tags…) works exactly as before. ## The loop ```bash theme={null} curl "https://cool.jobmojito.com/functions/v1/job-interview-get?position_id=INTERVIEW_UUID" \ -H "Authorization: Bearer $SUPABASE_JWT" ``` The response's `questions` array is ordered exactly as candidates are asked: ```json theme={null} { "name": "Support Specialist", "questions": [ { "id": "3f2a…", "question": "Tell me about a time you de-escalated an angry customer.", "duration": 120, "is_multiple_choice": false, "…": null }, { "id": "9c1b…", "question": "How do you prioritize a full support queue?", "duration": 90, "…": null } ] } ``` Each `id` is the question's real identifier. **Keep the ids of questions you did not mean to change** — that is how the update recognises them. Work on the array you just read: * **Change a question** — edit its text (or any field) and keep its `id`. * **Remove one** — delete the entry. * **Add one** — append an entry with no `id`. * **Reorder** — move entries around. ```bash theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-update \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "position_id": "INTERVIEW_UUID", "questions": [ { "id": "9c1b…", "question": "How do you prioritize a full support queue?", "duration": 90 }, { "id": "3f2a…", "question": "Tell me about a time you turned around an angry customer." }, { "question": "Which ticketing systems have you used?", "duration": 60 } ] }' ``` Send the **complete list you want the interview to end up with** — there is no way to change a single question on its own. An empty array is rejected: an interview needs at least one question. The response tells you exactly what happened, so you never have to guess. `questions_diff` is present only when you sent `questions` — it is absent from the response otherwise: ```json theme={null} { "position_id": "INTERVIEW_UUID", "updated_fields": ["questions"], "questions_diff": { "kept": ["9c1b…"], "replaced": [{ "from": "3f2a…", "to": "7d4e…" }], "added": ["b820…"], "removed": [], "question_ids": ["…full ordered step list…"], "reactivated": true } } ``` ## How your array is matched The array is applied as a **diff, not a replace**. Each entry is matched against what is stored, trying in order: 1. **`external_id`** — your own stable identifier, when both sides carry one. This lets an ATS send its own array without ever storing our ids. 2. **`id`** — the question id `job-interview-get` returned. 3. **Identical content** — so an array regenerated without ids still recognises the questions that did not actually change. Then, per entry: | Situation | What happens | | - | - | | Matches a question, nothing differs | **Kept** — the question record is untouched | | Matches a question, something differs | **Replaced** — the old one is unlinked, a new one created in its place | | Matches nothing | **Added** — a new question is created | | A stored question no entry matched | **Removed** — unlinked from this interview | **Resending the array you just read changes nothing.** If the diff finds nothing to do, no write happens at all — `updated_fields` comes back without `questions`, and the interview's `updated_at` is not even bumped. That makes the read → edit → write loop safe to run repeatedly, and safe for an agent to run when it is not sure whether anything changed. ## Why editing replaces a question Questions are **shared records**: the same question can be used by more than one interview. So this endpoint never edits one in place — that would silently rewrite the question inside every other interview using it. Instead it unlinks the old record and creates a new one, exactly like **Duplicate & Edit** in the admin. Two consequences worth knowing: * **Nothing is ever deleted.** Removing a question only unlinks it from this interview; the record itself survives, and so do the results that reference it. * **Keeping a question keeps everything attached to it** — its answer rules, its rendered avatar video, and the fields this API does not expose (coach keywords, categories, and so on). That is the real reason to send back the `id`s: a question you keep costs nothing and loses nothing, while a replaced one starts fresh. Fields you leave out of an edited entry are **carried over from the question you are replacing** — so `{ "id": "3f2a…", "question": "New wording" }` keeps the old duration, label and `external_id` rather than clearing them. ## Editing a live interview | Interview status | Template type | Can you change questions? | | - | - | - | | `draft` | any | ✅ Yes | | `active` | `interactive_heygen`, `interactive_elevenlabs` | ✅ Yes — re-published automatically | | `active` | `offline_heygen` (and legacy `offline_*`) | ❌ **422** — set it back to `draft` first | On the interactive templates there is no video to render, so the interview is re-published for you and the new questions go live immediately (`questions_diff.reactivated` is `true`). The offline templates pre-render a video per question, so a live one has to go back to `draft` before its questions can change: ```bash theme={null} # 1. unpublish and edit in one call — status:"draft" is applied before the questions curl -X POST …/job-interview-update \ -d '{"position_id":"…", "status":"draft", "questions":[…]}' # 2. publish again, which renders the video for each new question curl -X POST …/job-interview-update \ -d '{"position_id":"…", "status":"active"}' ``` Changing the questions of an interview candidates have already completed does not rewrite their results — those still refer to the questions that were asked at the time. If you need a clean comparison across candidates, create a new interview instead of editing a live one. ## Personas: the array is the opening line A persona has no question list — but its **opening line** is stored as a listening step, so it is the single entry in `questions`: ```json theme={null} { "type": "persona", "questions": [ { "id": "90bdfa65…", "question": "Look, I have about two minutes. What is it you actually want?", "duration": null, "…": null } ] } ``` This holds for both variants (`persona` and `persona_interview`), and it makes editing a persona's opening line straightforward: send that one entry back with its `id` and new text. Because it is the only entry, dropping it — or sending an array that does not match it — replaces the line the avatar opens the scene with. Adding further entries adds spoken steps to a format that is meant to be free-form conversation. When editing a persona, send exactly one entry. A persona's `duration` reads `null` here while `calc_definition_json` shows `exp_duration: 60` — the compiler applies that default at build time rather than storing it. Sending `null` back is therefore still a no-op, not a change. ## What this does not change * **The welcome and thank-you messages, and the instructional-video screen.** They are stored as steps rather than questions, are not part of the `questions` array, and are left in place — including your custom text, and including their position at the start and end of the interview. Set them at creation with `welcome_message` and `thank_you_message`. * **The language.** `mojito_language_code` is fixed after creation — the existing questions and any rendered videos are already in it. Create a new interview to change language. (An individual question may still override it.) * **Multi-stage positions.** A position has no question list of its own; fetch and update its interview stages individually. ## Re-deriving the scoring rubric The interview-level `candidate_expectations_json` rubric is derived from the questions, so a big change to the list can leave it out of date. Pass `regenerate_candidate_expectations: true` alongside `questions` to re-derive it from the resulting list, the way creation does: ```json theme={null} { "position_id": "INTERVIEW_UUID", "questions": [ "…" ], "regenerate_candidate_expectations": true } ``` It applies only to `type: "interview"`, and sending your own `candidate_expectations_json` in the same call wins over the regeneration. ## Next steps Start from a job description, your own questions, or a role-play persona. Turn your interview into links or email invitations. # Format content with Markdown Source: https://developer.jobmojito.com/cookbooks/format-content-with-markdown The Markdown vocabulary JobMojito renders — headings, tables, code, plus chips, callouts, cards, columns and buttons. Several JobMojito fields are rendered as Markdown rather than shown as plain text. This page is the complete list of what you can write in them and what the candidate sees. ## Fields that render Markdown | Field | Endpoint | Where the candidate sees it | | - | - | - | | `content_md` | `catalogue-tag-create`, `catalogue-tag-update` | A coaching catalogue directory page | | `description_long` | `job-interview-create`, `job-interview-update`, `job-interview-create-from-array` | The position / session detail page | | `position_description_long` | `job-interview-create-for-candidate-with-token` | The position detail page of the interview it creates | Read the current value back with `catalogue-tag-get` (`content_md`) or `job-interview-get` (`interview_description_long`). Every field above uses the same vocabulary and the same renderer. Only `content_md` additionally accepts the catalogue directives at the end of this page, because only a directory page has sessions to place. These fields are **replaced wholesale**, not merged. Read the current value, edit the whole thing, and send the whole thing back. ## Sending Markdown over the API Markdown is line-based and JSON strings are not, so newlines have to be escaped as `\n` in the request body: ```bash cURL theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/catalogue-tag-update \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "id": "sales-coaching-en", "content_md": "## Sales coaching\n\nStart with discovery.\n\n[chip:6 sessions] [chip:Beginner,tone=accent]\n\n[sessions:filter=discovery,limit=6]\n" }' ``` ```python Python theme={null} import json, requests content_md = """## Sales coaching Start with discovery. [chip:6 sessions] [chip:Beginner,tone=accent] [sessions:filter=discovery,limit=6] """ requests.post( "https://cool.jobmojito.com/functions/v1/catalogue-tag-update", headers={"Authorization": f"Bearer {token}"}, json={"id": "sales-coaching-en", "content_md": content_md}, ) ``` ## Standard Markdown ### Headings ```markdown theme={null} # Page title ## Section ### Subsection ``` Bold text inside a heading is highlighted in your portal's accent colour, so `## The **fast** track` gives you a coloured word rather than a bolder one. A heading is given twice as much space above it as below it, so it reads as belonging to the section it opens. A heading at the very top of the content gets no space above it, and two headings in a row are treated as a title and its subtitle and kept close together. ### Text ```markdown theme={null} **bold**, *italic*, ~~struck through~~, `inline code` A blank line starts a new paragraph. Two spaces at the end of a line force a line break inside one. ``` ### Lists ```markdown theme={null} - Bullet - Another bullet - Nested bullet 1. First 2. Second - [ ] Task not done - [x] Task done ``` ### Links and images ```markdown theme={null} [Link text](https://example.com) [Internal link](/catalogue) ![Alt text](https://example.com/picture.png) ``` Images are constrained to the width of the column and get rounded corners automatically. ### Quotes ```markdown theme={null} > A quotation. ``` For a highlighted aside, use `[note]` instead — see below. ### Tables ```markdown theme={null} | Session | Length | Level | | --- | --- | --- | | Screening | 12 min | Beginner | | Deep dive | 40 min | Advanced | ``` Wide tables scroll sideways inside their own box rather than stretching the page. ### Code Fence a block with three backticks and, optionally, a language name: ````markdown theme={null} ```sql select count(*) from results; ``` ```` The block renders on a dark background, with the language shown above it and a copy button. Nothing inside a fence is interpreted, which is how you show an example of the syntax itself. Indented code blocks are **not** supported — always use fences. ### Horizontal rule ```markdown theme={null} --- ``` ## Rich elements These are written as `[name]` or `[name:argument]`. An argument is either a single value or a comma-separated list of `key=value` pairs. ### Chip — inline A small pill for a label, a level, a duration or a status. Write it anywhere in a sentence. ```markdown theme={null} [chip:Beginner] [chip:6 sessions] [chip:New,tone=accent] [chip:Sold out,tone=danger] ``` `tone` is one of `neutral` (the default), `accent`, `success`, `warning`, `danger`, `info`. ### Keyboard key — inline ```markdown theme={null} Press [kbd:Ctrl] + [kbd:K] to search. ``` ### Note — block A tinted aside. Put it on its own lines and close it with `[/note]`. The body is ordinary Markdown. ```markdown theme={null} [note:tip] Book fifteen minutes a day. Short and regular beats one long session. [/note] ``` With a heading: ```markdown theme={null} [note:warning,title=Before you book] Sessions cannot be rescheduled within 24 hours. [/note] ``` `tone` is one of `note` (the default), `tip`, `info`, `warning`, `danger`. `caution` and `important` are accepted as spellings of `warning`, and `success` as a spelling of `tip`. The title is optional and is shown exactly as you type it — there is no default label, so a note carries no English wording unless you write it. Put the title in the same language as the rest of the content. ### Card — block A bordered, raised box around any Markdown. ```markdown theme={null} [card] ### Screening simulation Twelve minutes, with feedback on structure and clarity. [/card] ``` ### Columns — block Lays its children out side by side: one column on a phone, two from tablet width up, and the requested number on a wide screen. ```markdown theme={null} [columns:3] [card] ### Fast Twelve minutes. [/card] [card] ### Thorough Forty minutes. [/card] [card] ### Custom Bring your own brief. [/card] [/columns] ``` `[columns:2]`, `[columns:3]` and `[columns:4]` are supported; anything else falls back to two. Each block inside becomes one cell. To put several blocks in one cell, wrap them in `[column] … [/column]`: ```markdown theme={null} [columns:2] [column] ## What you get - A recorded session - A written report [/column] [column] ## What you need Twenty quiet minutes and a working microphone. [/column] [/columns] ``` ### Button — block A call to action. Needs both a label and an `href`. ```markdown theme={null} [button:Book a session,href=/catalogue,tone=accent] [button:Read the guide,href=https://example.com] ``` `tone` is one of `primary` (the default, dark), `accent` (your brand colour) or `secondary` (outlined). External links open in a new tab. Two buttons written on consecutive lines sit next to each other. ### Divider — block ```markdown theme={null} [divider] ``` A quieter rule than `---`, with more space around it. ## Catalogue directives `content_md` only. Each must sit alone on its line, and each draws from the sessions and sub-directories the directory already contains — a directive can never surface a session the directory itself would not show. ```markdown theme={null} [plan-progress] the learner's coaching-plan progress [directory:sales-coaching-closing-en] one sub-directory card [session:screening-en] one session card, by id or code [sessions] every session in this directory [sessions:negotiation] sessions matching a term [sessions:filter=negotiation,limit=6] the same, capped ``` `filter` matches a session's tags first (whole tags, so `new` does not match `renewal`), then its name, description and code. Accents are ignored, so `filter=simulacion` finds "Simulación". Several terms can be given, separated by commas. Card directives written on consecutive lines share one row: ```markdown theme={null} [session:screening-en] [session:negotiation-en] [session:panel-en] ``` If `[sessions:filter=…]` renders nothing, check `matched_sessions` from `catalogue-tag-get` first — the filter narrows that set, it does not widen it. See [Build a coaching catalogue](/cookbooks/build-a-coaching-catalogue) for how sessions get into a directory in the first place. ### A worked directory page ```markdown theme={null} # Negotiation programme [chip:6 sessions] [chip:Beginner,tone=accent] Practise asking for what you are worth, from the first number to the final handshake. [plan-progress] ## Start here [sessions:filter=negotiation-basics,limit=3] [note:tip] Do these in order — each one assumes the last. [/note] ## Go deeper [sessions:filter=negotiation-advanced] [divider] [button:See the whole catalogue,href=/catalogue,tone=accent] ``` ## HTML Plain HTML works for anything Markdown cannot express: ```markdown theme={null}
Centred
Text with a coloured word in it. ``` Four things to know: * `style` is written the ordinary HTML way, as a string. It is converted for you. * `class`, `colspan`, `tabindex` and the rest of the HTML attribute names are accepted and translated. * Void tags may be written either way — `
` and `
` both work. * The content is sanitised before it reaches a candidate's browser, so it cannot run scripts. What is removed: | Removed | What it covers | | - | - | | Tags | `script`, `object`, `embed`, `applet`, `base`, `frame`, `frameset`, `portal` | | Attributes | every `on…` handler, plus `srcdoc` and `ping` | | URLs | anything in `href`, `src`, `action`, `formaction`, `poster`, `cite`, `data`, `background`, `srcset` or `xlink:href` that is not `http`, `https`, `mailto` or `tel` — so `javascript:` links never render. A `data:image/…` source is allowed on an ``. | This is applied on render, to content already stored. If you write a blocked tag it is accepted by the API and saved — it simply does not appear on the page. ## Things that trip people up **Curly braces are safe.** `{name}` renders as `{name}`. You do not need to escape it. **A block element must be alone on its line.** `Some text [sessions]` renders the literal text. Chips and `[kbd:…]` are the exception — those are inline by design. **Unknown names stay as text.** `[see the guide]` on its own line renders as written, because `see the guide` is not one of the names above. **Nothing inside a code fence is interpreted**, including the elements on this page. That is how you show the syntax to someone else. **Values may contain spaces, but not commas.** `[button:Book a session,href=/x]` is fine; a label with a comma in it will be cut short. **Backslashes in stored content are expected.** Content that was last saved through the JobMojito admin editor comes back with backslashes in front of punctuation — `\[sessions\:negotiation]`. That is the editor's own escaping and it renders correctly. Leave it as it is; do not strip it by hand. **Close your containers.** An unclosed `[card]` wraps everything after it rather than blanking the page, but it will not look the way you meant. # Invite candidates Source: https://developer.jobmojito.com/cookbooks/invite-candidates Generate signed interview links or send branded email invitations for an interview. Once you have an interview (`interview_def_set_id` from [Create an interview](/cookbooks/create-an-interview)), there are two ways to get candidates in: | Goal | Use | MCP tool | | - | - | - | | Get interview **URLs** back, or send the **email** invite chain | `POST /job-interview-register-users` | `register_users_for_interview` | | Invite people to the merchant (candidates, mentors, teammates) | `POST /invite-users` | `invite_users` | **Interview role-plays** — the interview-portal variant of a [role-play persona](/cookbooks/create-an-interview) — are invited exactly the same way. Pass their `interview_def_set_id` as the `interview_id` below; nothing else changes. ## Generate links or send invitations `POST /job-interview-register-users` handles both, switched by `type`: * `type: "url"` — generates a public interview URL (with an embedded JWT) per user. You redirect/send it yourself. No email is sent. * `type: "invitation"` — also subscribes the user to JobMojito's email chain (invitation, reminders, and completion notifications). Each user needs `name` and `email`. Optional: `external_id` (your own id, echoed back) and `phone` (international E.164 format, e.g. `+421903123456`) for SMS / WhatsApp notifications. A phone already stored on the candidate is never overwritten. Required fields: `interview_id`, `type`, `hide_menu`, and `users`. ```bash Get URLs (type: url) theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-register-users \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "interview_id": "9c1b…e4f2", "type": "url", "hide_menu": false, "users": [ { "name": "Peter Parker", "email": "peter@parker.com", "external_id": "abcd" }, { "name": "MJ Watson", "email": "mj@example.com" } ] }' ``` ```bash Send email invites (type: invitation) theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-register-users \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "interview_id": "9c1b…e4f2", "type": "invitation", "hide_menu": false, "users": [ { "name": "Peter Parker", "email": "peter@parker.com" } ] }' ``` The response is an array mirroring your input, with an `interview_url`, `profile_interview_id`, and a `result` per user (`ok` or `Error: …`). ```json theme={null} [ { "name": "Peter Parker", "email": "peter@parker.com", "external_id": "abcd", "interview_url": "https://…", "profile_interview_id": "19bd…bae4", "result": "ok" } ] ``` ### Fields * `is_test: true` mints **test** links. They also work on a draft interview (the current questions are compiled first), and the result is flagged as a test. Use it with a `uat` interview and webhook. See [Environments and testing](/how-the-api-works#environments-and-testing). * **SMS / WhatsApp.** When the interview's `candidate_notification_channel` is set (`all`, `reminders` or `last_reminder`, on `job-interview-update`), candidates with a `phone` also receive the invitation and/or reminders by WhatsApp, with SMS as the fallback. Messages are charged at the carrier price (1 credit per €1). This needs a paid plan. Candidate invitations consume merchant interview credits. Each user is processed independently — one failing user returns an `Error: …` result for that row while the others still succeed. ## Alternative: invite to the merchant `POST /invite-users` invites people to the merchant and can attach candidates to an interview. It also supports `mentor`, `merchant`, and `merchant_owner` types. ```bash cURL theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/invite-users \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "users": [ { "name": "Peter Parker", "email": "peter@parker.com", "type": "candidate", "interview_id": "9c1b…e4f2" } ], "message_from_recruiter": "We'd love you to complete this short interview." }' ``` For `type: "candidate"`, `interview_id` is required. Optional: `force_invite` (resend pending invites), `mojito_language_code` (defaults `en`), and `message_from_recruiter`. Each user comes back with a `result` such as `Invited`, `Activated`, `Skipped`, or `Error: …`. ## Next steps Track who finished and read their scored transcripts. Score résumés before spending interview credits. # Manage knowledge base Source: https://developer.jobmojito.com/cookbooks/manage-knowledge-base Upload documents so interviews can ask grounded, knowledge-based questions. A knowledge base lets interviews ask questions grounded in your own documents (product manuals, policies, process docs). Upload documents to a knowledge base store, then reference that store when creating an interview. | Step | Endpoint | MCP tool | | - | - | - | | Upload a document | `POST /knowledge-base-document-upload` | `upload_knowledge_base_document` | ## Upload a document The endpoint accepts either a `multipart/form-data` upload (binary file) or `application/json` (base64-encoded file). Both require a `knowledge_base_store_id`. ```bash multipart/form-data theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/knowledge-base-document-upload \ -H "Authorization: Bearer $SUPABASE_JWT" \ -F "knowledge_base_store_id=STORE_UUID" \ -F "name=onboarding-policy.pdf" \ -F "file=@./onboarding-policy.pdf" ``` ```bash application/json (base64) theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/knowledge-base-document-upload \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "knowledge_base_store_id": "STORE_UUID", "name": "onboarding-policy.pdf", "file": "JVBERi0xLjQKJ…(base64)…" }' ``` The response returns `knowledge_base_id` for the created record. The document is then queued for processing (parsing + indexing) asynchronously. ## Use the knowledge base in an interview Once documents are processed, reference the store when you [create an interview](/cookbooks/create-an-interview): * **Whole interview** — set `knowledge_base_store_id` on `job-interview-create` or `job-interview-create-from-array`. * **Per question** — set `knowledge_base_id` on an individual item in the `questions` array (Option B) so only that question is grounded in the store. Use knowledge-base grounding with `interview_type: "process-verification-from-knowledge-base"` to verify a candidate's understanding of your documented processes. ## Next steps Reference your store to ask grounded questions. See how candidates performed on knowledge-based questions. # Branding Source: https://developer.jobmojito.com/cookbooks/portal-branding/branding The branding document: portal name, style, colours, logos, e-mail sender name and speech term boosting. ## Branding configuration Generates the document imported by **Portal customisation → Branding → Import configuration**. Output a single JSON object. Every field is optional: whatever you omit keeps its current value in the portal. Nothing is written to the database on import — the operator reviews the result in the form and presses **Save**. ## Document ```json theme={null} { "kind": "branding", "version": 1, "portal_name": "Northgate Nursing Simulation", "business_description": "Clinical simulation training for undergraduate nursing students.", "style": "minimal", "report_template": "modern", "colors": { "primary": "212 84% 38%", "primary_opposite": "0 0% 100%", "secondary": "18 92% 60%", "background": "210 33% 98%", "font_black": "0 0% 17%", "font_grey": "0 0% 48%" }, "logo_url": "https://cdn.example.org/northgate/logo.svg", "icon_url": "https://cdn.example.org/northgate/icon.png", "logo_external_url": "https://northgate.example.org", "email_sender_name": "Northgate Simulation", "show_demo_data": false, "speech_term_boosting": [ "auscultation", "tachycardia", "Northgate Nursing", "myocardial infarction" ] } ``` `kind` is optional, but when present it must be `"branding"`: a document declaring another kind is refused, so a coaching document pasted into the branding tab fails loudly instead of half-applying. Without `kind` the import still runs, with a warning that the type could not be verified. ## Fields | Field | Type | Notes | | - | - | - | | `portal_name` | string | Shown as the portal's name; used in page titles and e-mail. | | `business_description` | string | Internal description of the organisation. Not shown to learners or candidates. | | `style` | `"bold"` \| `"minimal"` | Rejected if any other value. See below. | | `report_template` | `"classic"` \| `"modern"` \| `"one_pager"` | Default layout of exported interview reports. Rejected if any other value; omitted keeps the current choice (`modern` when none was made). | | `spatius_enabled` | boolean | Offers the experimental 3D avatar template type in the admin. Off by default; leave it out unless asked. | | `colors.*` | string | HSL channels without the `hsl()` wrapper — see below. | | `logo_url` | url | The regular logo, for light backgrounds. Must already be hosted: the importer does not upload files. | | `icon_url` | url | Favicon / app icon. | | `logo_external_url` | url | Where the logo links to on the home page. Omit to keep the logo linking to the portal. | | `email_sender_name` | string | From-name on automated e-mail. | | `show_demo_data` | boolean | Demo data in the admin. Normally `false` for a real portal. | | `speech_term_boosting` | string\[] | Terms the speech-to-text engine should favour. See below. | ### style * **bold** — serif display headings, large type, gradient accents, dark position posters. * **minimal** — headings drop to the body font at reduced sizes, gradients collapse to flat colour, white cards instead of dark posters. This is a single switch across the whole portal; there is no per-page override. A portal with its own [theme CSS](/cookbooks/portal-customisation/portal-pages#theme-css) usually wants `minimal`, so the theme is not fighting the gradients. ### Colour format Colours are space-separated HSL channels, not hex and not `hsl(...)`: ```text theme={null} "212 84% 38%" correct "#1565c0" WRONG — stored as is and renders as an invalid colour "hsl(212 84% 38%)" WRONG ``` Convert hex to HSL before writing the document (round each channel to a whole number). The values are injected straight into CSS custom properties, so an invalid string produces an unstyled portal rather than an error. | Key | Used for | | - | - | | `primary` | Buttons, links, accents (`--accent` / `--primary`). The brand colour. | | `primary_opposite` | Text placed on top of `primary` — it must contrast with it, normally white or near-black. Getting this wrong is the most common cause of unreadable buttons. | | `secondary` | Secondary accents and gradients. | | `background` | Page background of every portal screen, including the interview and application screens. | | `font_black` | Main text colour. | | `font_grey` | Secondary text colour. | A portal theme ([theme CSS](/cookbooks/portal-customisation/portal-pages#theme-css)) reads these through `var(--accent)`, so the brand colour set here stays the single source of truth even when the pages are restyled. ### Speech term boosting Domain vocabulary that speech-to-text would otherwise mishear. Supply the words a learner or candidate will actually say out loud that a general-purpose recogniser gets wrong: clinical terms, drug names, product names, the organisation's own name. Constraints, enforced on import exactly as the form enforces them: * **5 to 50 characters per term.** Shorter or longer terms are dropped and reported. This rules out most short abbreviations — `ECG` is too short to qualify, `electrocardiogram` is fine. * **Maximum 100 terms.** Beyond that the first 100 are kept and the rest reported. * **Only supported in some speech languages.** The branding tab lists which; a term set for an unsupported language is simply inert. Quality matters more than quantity. A long list of common words dilutes the boost and can make recognition worse, so prefer 10–40 genuinely domain-specific terms over a hundred generic ones. Write each term as it is spoken, in normal capitalisation. ## Not in this document These are set in the admin by hand; the import neither reads nor changes them: | Setting | Where | | - | - | | **Logo on dark backgrounds** — a white or light version of the logo that a portal theme shows in a dark header | Branding, under the regular logo. Upload the file there. | | **Scheduling** (meeting steps and the recruiter calendar) and **Portal type** (interview portal or career page) | Portal customisation → Interview portal. | | Pages, the site footer and the theme CSS | [Portal pages](/cookbooks/portal-customisation/portal-pages), a separate import. | ## Rules * Reject the document rather than guess if `style` or `report_template` is not one of its values. * Do not invent colours to fill the palette. Supply only what the brand defines and let the rest keep their current values. * Check contrast between `primary` and `primary_opposite` before emitting. * Only reference logos that are already hosted at a public https URL. # Coaching portal Source: https://developer.jobmojito.com/cookbooks/portal-branding/coaching-portal The coaching portal document: vertical, feature toggles, wording, coaching plan, languages and per-language home content. ## Coaching portal configuration Generates the document imported by **Portal customisation → Coaching portal → Import configuration**. This is the largest of the three documents: it covers the portal's vertical, its feature toggles, its coaching plan, and the home content for every language. Every field is optional — omitted fields keep their current value. Languages are only ever added, never removed, so an import cannot delete existing translated content. Since the **Pages** editor (Configuration → Consumer portal), the support page and the signed-in home are **built-in pages**. This import still accepts `support_page_markdown` and `signed_in_home_blocks`, but writes them into those pages' drafts — see [Where each field goes](#where-each-field-goes). For additional pages, a menu, a site footer or a theme, use the [portal pages import](/cookbooks/portal-customisation/portal-pages). ## Document ```json theme={null} { "kind": "coaching-portal", "version": 1, "use_case": "simulation", "features": { "interview_coach_enabled": true, "resume_check_enabled": false, "cover_letter_enabled": false, "trivia_enabled": false, "homepage_pricing_enabled": false, "purchase_credits_enabled": false }, "labels": { "catalogue": "Case library", "sessions": "Simulations", "plan": "Placement plan", "session-singular": "patient simulation", "session-plural": "patient simulations", "coach": "Simulated patient" }, "coaching_plan_tags": ["osce-basics-en", "osce-cardiology-en"], "catalogue_start_tag": "osce-home-en", "languages": ["en"], "pages": [ { "language": "en", "public_home_blocks": [ { "id": "hero", "type": "hero", "kicker": "Clinical simulation", "title": "# Practise with _real_ patients, safely", "body": "Talk to an AI patient, get scored, repeat until it is second nature.", "ctaLabel": "Start training", "ctaHref": "/home" }, { "id": "stats", "type": "stats", "items": [ { "value": "120", "label": "Case scenarios" }, { "value": "24/7", "label": "Available" } ] } ], "signed_in_home_blocks": [ { "id": "notice", "type": "rich-text", "body": "Start with the cardiology cases before Thursday's assessment.", "boxed": true } ], "support_page_markdown": "## Getting help\n\nContact simulation@example.org." } ] } ``` `kind` is optional, but when present it must be `"coaching-portal"`; a document declaring another kind is refused. ## Top-level fields | Field | Type | Notes | | - | - | - | | `use_case` | enum | `job-seeker`, `corporate`, `simulation`, `education`, `custom`. Sets the default wording everywhere. Rejected if any other value. | | `features.*` | boolean | Only the six keys above are recognised; anything else is reported as ignored. | | `labels.*` | string | Overrides the preset wording. Only the six keys above are recognised. | | `coaching_plan_tags` | string\[] | Ordered `catalogue_tag` ids. See the warning below. | | `catalogue_start_tag` | string | The directory the catalogue opens on. | | `languages` | string\[] | Language codes to enable. | | `pages[]` | object\[] | Per-language content, keyed by `language`; entries without it are skipped. | ### use\_case and labels `use_case` picks a vocabulary preset; `labels` overrides individual words on top of it. Prefer the preset and override only where the organisation genuinely uses different terminology. Overrides are portal-wide, not per language, so if the portal runs several languages, either leave `labels` out and let the preset translate, or accept that the override text appears in every language. ### coaching\_plan\_tags — must be live directories A directory only reaches learners when it is `visibility: public` and `status: active`. A plan step pointing at a draft or archived directory is **silently dropped** from the plan — the learner sees a shorter plan with no explanation. Only reference directories you have confirmed exist and are active (see [Build a coaching catalogue](/cookbooks/build-a-coaching-catalogue)). If you cannot confirm, leave `coaching_plan_tags` out entirely: with no plan configured the block is hidden rather than shown half-empty. ## Where each field goes | Fields | Goes to | Saved | | - | - | - | | `use_case`, `features`, `labels`, `coaching_plan_tags`, `catalogue_start_tag`, `languages`, `public_home_blocks` | The Coaching portal form. | **Not yet** — the operator reviews the form and presses **Save**. | | `support_page_markdown` | Built-in page `_support` (`/support`) | **Immediately, as a draft.** The text becomes one text block of that language, replacing the language's blocks; other languages stay. Nothing is published — the operator publishes it from **Pages**. | | `signed_in_home_blocks` | Built-in page `_signed_in_home` (top of `/home`) | Same as above; the blocks replace that language's blocks. | ## Page blocks `public_home_blocks` renders the signed-out marketing home page. `signed_in_home_blocks` renders at the top of `/home` after sign-in — keep that one short, it sits above the learner's coaching. Each block needs an `id` (unique within its list) and a `type`. A block with an unknown type is skipped and reported; the rest of the import still applies. This document accepts the ten home-page block types below; the [portal pages import](/cookbooks/portal-customisation/portal-pages#blocks) accepts more (cards, tabs, video, …). Every block may also carry `enabled` (`false` hides it), `tone` (`default`, `muted`, `dark`, `accent` — the section background), `anchor` (id for `/#anchor` links) and `className` (hook for a theme). | type | Fields | | - | - | | `hero` | `kicker`, `title` (markdown), `body` (markdown), `ctaLabel`, `ctaHref`, `imageUrl`, `layout` (`split`\|`background`), `align` (`left`\|`center`), `height` (`medium`\|`tall`), `videoUrl` | | `stats` | `title`, `items[]` of `{ value, label }` | | `media-text` | `title`, `body` (markdown), `imageUrl`, `imageSide` (`left`\|`right`), `ctaLabel`, `ctaHref` | | `feature-grid` | `title`, `columns` (2–5), `imageStyle` (`cover`\|`icon`), `items[]` of `{ title, body, imageUrl, href, linkLabel }` | | `quote` | `quote`, `author`, `role`, `imageUrl`, `layout` (`centered`\|`photo`), `title` | | `logo-row` | `title`, `layout` (`row`\|`slider`), `logos[]` of `{ imageUrl, alt }` | | `rich-text` | `body` (markdown), `width` (`narrow`\|`wide`), `boxed` (boolean), `textStyle` (`headline`\|`body`) | | `pricing` | `title` — renders the configured pricing table | | `cta` | `title`, `body` (markdown), `ctaLabel`, `ctaHref`, `imageUrl` (background photo), `align` (`center`\|`left`) | | `faq` | `title`, `items[]` of `{ question, answer }` | Markdown fields are rendered with a restricted renderer: headings, bold, italic, lists, links (see [Format content with Markdown](/cookbooks/format-content-with-markdown)). In a hero `title`, wrapping a word in `_underscores_` renders it as the italic accent word. Images must be URLs that are already hosted. The importer uploads nothing. ## Rules * Put the primary language first in `pages[]` — the editor opens on it after import. * Blocks render in array order; that order is the page. * A hero `ctaHref` of `/home` sends a signed-out visitor into sign-up. Use that unless there is a reason not to. * Do not set `homepage_pricing_enabled: true` unless the portal actually sells credits; the pricing block renders empty otherwise. * Tell the operator that the support page and the signed-in home land as drafts in **Pages** and need publishing there. # Interview portal Source: https://developer.jobmojito.com/cookbooks/portal-branding/interview-portal The interview portal document: languages, the classic home page texts, and the support, tracking and completion messages. ## Interview portal configuration Generates the document imported by **Portal customisation → Interview portal → Import configuration**. The interview portal is the candidate-facing side: the public job list and position pages, the apply flow, and the screens a candidate sees around an interview. It is a different product from the coaching portal and has its own languages and content. Every field is optional — omitted fields keep their current value. Languages are only ever added, never removed. Since the **Pages** editor (Configuration → Interview portal), the support, application-tracking and interview-completed texts are **built-in pages**. This import still accepts them, but writes them into those pages' drafts — see [Where each field goes](#where-each-field-goes). To build full pages, a menu, a site footer or a theme, use the [portal pages import](/cookbooks/portal-customisation/portal-pages) instead. ## Document ```json theme={null} { "kind": "interview-portal", "version": 1, "languages": ["en"], "pages": [ { "language": "en", "hero_box": "# Join the team at _Northgate_\n\nEvery application is reviewed by a person.", "about_box": "## About us\n\nWe run 14 clinics across the region.", "quote": "The interview felt like a conversation, not an interrogation.", "readmore_text": "Read about our hiring process", "readmore_link": "https://northgate.example.org/careers/process", "readmore_button": "How we hire", "interview_completed_box": "Thanks — we will be in touch within five working days.", "application_tracking_box": "Track your application any time using the link in your email.", "support_page": "## Help\n\nEmail careers@example.org.", "public_avatar_id": null, "discover_id": null, "public_interviews_disabled": false, "disable_easy_apply": false } ] } ``` `kind` is optional, but when present it must be `"interview-portal"`; a document declaring another kind is refused. ## Fields | Field | Type | Where it appears | | - | - | - | | `languages` | string\[] | Language codes to enable on the interview portal. | | `pages[].language` | string | Required on every page entry; entries without it are skipped. | | `hero_box` | markdown | Hero of the portal's classic home page. | | `about_box` | markdown | "About the company" section of the classic home page. | | `quote` | string | Pull quote on the classic home page. Plain text, no markdown. | | `readmore_text` | string | Label of the read-more block. | | `readmore_link` | url | Its target. | | `readmore_button` | string | Its button text. | | `interview_completed_box` | markdown | Shown after a candidate finishes an interview (`/interview/success`). | | `application_tracking_box` | markdown | Shown on the application-tracking screen (`/application-tracking`). | | `support_page` | markdown | The portal's support page (`/support`). | | `public_avatar_id` | uuid \| null | Talking avatar on the classic home page. Leave `null` unless you have a real id. | | `discover_id` | uuid \| null | Discovery interview offered beside the job openings. Leave `null` unless you have a real id. | | `public_interviews_disabled` | boolean | `true` hides publicly listed interviews. | | `disable_easy_apply` | boolean | `true` removes the one-click apply route. | ## Where each field goes The document touches two places, and they behave differently: | Fields | Goes to | Saved | | - | - | - | | `languages`, `hero_box`, `about_box`, `quote`, the read-more trio, `public_avatar_id`, `discover_id`, `public_interviews_disabled`, `disable_easy_apply` | The Interview portal form. | **Not yet** — the operator reviews the form and presses **Save**. | | `support_page` | Built-in page `_support` | **Immediately, as a draft.** Each text becomes one text block of that language, replacing the language's blocks; the page's other languages stay. Nothing is published — the operator publishes it from **Pages**. | | `application_tracking_box` | Built-in page `_tracking` | Same as above. | | `interview_completed_box` | Built-in page `_completion` | Same as above. | ### The classic home page `hero_box`, `about_box`, `quote`, the read-more block, `public_avatar_id` and `discover_id` make up the portal's **classic home page**. They show only while the home page has not been customised in **Pages**. Once the operator presses *Customise home page*, those sections become editable blocks of the page and the page wins — importing these fields afterwards changes nothing a candidate sees. For a customised home page, import the page itself (slug `""`) with the [portal pages import](/cookbooks/portal-customisation/portal-pages). ### The read-more block `readmore_text`, `readmore_link` and `readmore_button` are one unit. Supply all three or none — a partial set renders a block with a missing label or a button that goes nowhere. ### Ids `public_avatar_id` and `discover_id` are database ids. **Never invent them.** If you do not have a confirmed id, emit `null` or omit the field. A fabricated id produces a broken page rather than a validation error, because the importer cannot check ids that live in another table. ### Markdown Rendered with a restricted renderer: headings, bold, italic, lists, links. In `hero_box`, wrapping a word in `_underscores_` renders it as the italic accent word. Keep `interview_completed_box` short — it is read once, by someone who has just finished an interview and wants to know what happens next. Say what happens and when. ## Not in this document Set by hand in Portal customisation → Interview portal: **Portal type** (interview portal or career page — a career page shows positions as job ads and calls them "Positions" in the admin), **Scheduling**, the SMS / WhatsApp sender, and the candidate e-mail flow. Pages, the site footer, the menu and the theme CSS come from the [portal pages import](/cookbooks/portal-customisation/portal-pages). ## Rules * Put the primary language first in `pages[]` — the editor opens on it after import. * Write candidate-facing copy in second person ("your application"), not third ("the candidate's application"). * Do not set `public_interviews_disabled: true` on a portal whose whole purpose is a public careers page; check before emitting it. * Tell the operator that the support, tracking and completion texts land as drafts in **Pages** and need publishing there. # Portal pages Source: https://developer.jobmojito.com/cookbooks/portal-branding/portal-pages Import a whole portal site — pages in every language, the menu, the site footer and the theme CSS — as one JSON document. ## Portal pages import Generates the documents imported by **Configuration → Interview portal** (or **Consumer portal**) **→ Import** in the admin. A portal site is a set of **pages**. Each page has an address (its **slug**), an optional place in the menu, and per language a title, an SEO description and an ordered list of **blocks** — the same blocks the visual page editor builds. A site can also carry **theme CSS** that restyles the whole portal. One import can create a single page or rebuild a complete careers site, for example a copy of an existing corporate careers website with its sub-pages. This import writes pages to the database **as drafts** straight away (unlike the three configuration documents, which only fill a form). Nothing reaches candidates until the page is published — either with the **Publish after import** switch in the import dialog, or page by page afterwards. ## Documents Two document shapes. A file may hold one document or a JSON array of them; the dialog also takes several files at once. ### A whole site ```json theme={null} { "format": "jobmojito.portal-site", "version": 1, "portal": "interview", "theme": { "css": ".jm-site .jm-nav { background: #000033; }" }, "pages": [ { "slug": "", "show_in_menu": false, "sort_order": 0, "languages": [ … ] }, { "slug": "about", "show_in_menu": true, "sort_order": 10, "languages": [ … ] }, { "slug": "about/our-history", "show_in_menu": true, "sort_order": 11, "languages": [ … ] }, { "slug": "_footer", "show_in_menu": false, "sort_order": 999, "languages": [ … ] } ] } ``` | Field | Type | Notes | | - | - | - | | `format` | `"jobmojito.portal-site"` | Required. | | `version` | `1` | A higher version is imported as far as this editor understands it, with a warning. | | `portal` | `"interview"` \| `"consumer"` | Which portal the site was made for. Importing into the other one works but shows a warning. | | `theme.css` | string | Theme CSS for the portal — see [Theme CSS](#theme-css). Omit to keep the current theme. | | `pages` | Page\[] | See [Page](#page). | ### One page ```json theme={null} { "format": "jobmojito.portal-page", "version": 1, "page": { "slug": "careers/culinary", "show_in_menu": false, "languages": [ { "mojito_language_code": "en", "title": "Culinary", "description": "Culinary jobs at sea and ashore.", "blocks": [ { "id": "hero", "type": "hero", "layout": "background", "title": "Culinary", "imageUrl": "https://cdn.example.org/culinary.jpg", "showSearch": true }, { "id": "jobs", "type": "jobs", "title": "Culinary jobs", "tags": ["culinary"], "showSearch": true, "emptyText": "No culinary jobs are open right now." } ] } ] } } ``` ## What an import does * **Matched by slug.** A page whose slug already exists is **replaced** (all its languages, its menu flag and, when given, its sort order). Any other page is **created**. Pages that are not in the document are left alone — an import never deletes a page. * **Same slug twice** — the last one in the document (or the last file) wins, with a warning. * **Draft or live.** Everything is saved as a draft. With **Publish after import** switched on, pages and the theme are published in the same step. * **Theme.** When the document has `theme.css`, it replaces the portal's theme draft (and is published with the switch on). * **Checked before writing.** The dialog lists every page it will create (`+`) or replace (`↻`), with its languages and block count, and any errors and warnings. Errors drop the affected page; warnings drop only the part named (a block, a language). * **Lenient about extra keys.** Unknown fields are ignored and unknown block types skipped with a warning, so a document written for a newer editor still imports what this one understands. ## Page | Field | Type | Notes | | - | - | - | | `slug` | string | Required. The page address — see [Slugs](#slugs). | | `show_in_menu` | boolean | Default `false`. See [Menu](#menu). | | `sort_order` | integer | Menu and list order, ascending. Omitted: an existing page keeps its order, a new page goes to the end. | | `languages` | Language\[] | At least one. One entry per portal language; a language listed twice keeps the first. | **Language entry** | Field | Type | Notes | | - | - | - | | `mojito_language_code` | string | Required. Platform language code: `en`, `de`, `sk`, `es`, … The portal shows the entry matching the visitor's language. | | `title` | string | Page title: browser tab, menu label, search results. | | `description` | string | SEO description. Empty falls back to the portal's default. | | `blocks` | Block\[] | The page content, top to bottom. See [Blocks](#blocks). | ## Slugs | Slug | Page | | - | - | | `""` | The portal home page (`/`). | | `about`, `life-onboard` | A page at `/about`, `/life-onboard`. Lower-case letters, digits and dashes. | | `about/our-history` | A sub-page at `/about/our-history`. Its parent `about` should exist. | | `_footer` | The **site footer**, shown on every page above the platform's legal footer. Holds one `footer` block. | | `_jobs` | Built-in: blocks shown above the job list at `/jobs` (interview portal). | | `_blog` | Built-in: blocks shown above the blog at `/blog` (while blogs are switched on). | | `_support` | Built-in: the support page at `/support`. | | `_tracking` | Built-in: the message on `/application-tracking` (interview portal, signed-in candidates). | | `_completion` | Built-in: the message after an interview, `/interview/success` (interview portal, signed-in candidates). | | `_signed_in_home` | Built-in: blocks at the top of `/home` (coaching portal, signed-in learners). | A custom slug is refused when: * it is not lower-case words joined by dashes, with `/` between levels (`^[a-z0-9]+(-[a-z0-9]+)*(/[a-z0-9]+(-[a-z0-9]+)*)*$`), or longer than 200 characters; * its first segment is an address the portal already uses: `jobs`, `blog`, `support`, `interview`, `position`, `login`, `profile`, `calendar`, `application-tracking`, `privacy_policy`, `terms_of_service`, `cookie_policy`, `home`, `catalogue`, `sessions`, `resume`, `pricing`, `faq` and a few more; * its first segment is a language code (`de/…` is the German version of a page, not a page called "de"). Built-in slugs (starting with `_`) and the home page cannot be moved or renamed; importing them only replaces their content. ## Menu * A top-level page with `show_in_menu: true` gets a link in the portal header, labelled with its title in the visitor's language. * Its direct sub-pages with `show_in_menu: true` form that link's dropdown. Deeper levels never appear in the menu — link to them from blocks. * `sort_order` orders both levels. * Built-in pages that can be in the menu (`_jobs`, `_blog`, `_support`, `_tracking`, `_signed_in_home`) follow their own `show_in_menu` once published. ## Blocks Every block is an object with a `type` and a stable `id` (unique within its language; generated when missing or duplicated). All blocks also accept: | Field | Values | Notes | | - | - | - | | `enabled` | boolean | Only `false` hides the block. | | `tone` | `default`, `muted`, `dark`, `accent` | Section background. A theme restyles each tone. | | `anchor` | `[a-z0-9-]*` | Id of the section, for links such as `/about#values`. | | `className` | letters, digits, dashes, spaces | Extra classes on the section wrapper, for the [theme CSS](#theme-css). | Fields marked *markdown* use the restricted renderer described in [Format content with Markdown](/cookbooks/format-content-with-markdown). Images and videos must be public https URLs; the importer uploads nothing. ### Content blocks | type | Fields | | - | - | | `hero` | `kicker`, `title` (markdown; `_word_` = accent), `body` (markdown), `ctaLabel`, `ctaHref`, `imageUrl`, `layout` (`split` image beside the copy \| `background` image behind it), `align` (`left`\|`center`, background layout), `height` (`medium`\|`tall` = full screen, background layout), `videoUrl` (looping .mp4/.webm over the image, background layout), `showSearch` (job search box, submits to `/jobs`), `searchPlaceholder`, `searchButtonLabel` | | `intro` | `body` (markdown), `imageUrl` (floated beside the text) | | `rich-text` | `body` (markdown), `width` (`narrow`\|`wide`), `boxed` (on a card), `textStyle` (`headline` large centred text \| `body` plain text) | | `media-text` | `title`, `body` (markdown), `imageUrl`, `imageSide` (`left`\|`right`), `ctaLabel`, `ctaHref` | | `cards` | `title`, `body` (markdown intro), `style` (`overlay` title over a photo \| `stacked` photo above text \| `tile` text-only link tiles), `columns` (2–4), `ratio` (`portrait`\|`landscape`\|`square`), `showTitles` (`false` hides titles on overlay cards whose photo carries a logo), `layout` (`grid`\|`slider` paged sideways), `items[]` of `{ title, body, imageUrl, href, linkLabel }` | | `feature-grid` | `title`, `columns` (2–5), `imageStyle` (`cover`\|`icon`), `items[]` of `{ title, body, imageUrl, href, linkLabel }` | | `stats` | `title`, `items[]` of `{ value, label }` | | `quote` | `quote`, `author`, `role`, `imageUrl`, `layout` (`centered`\|`photo` portrait across the section with the quote on a card), `title` (card heading, photo layout) | | `video` | `title`, `body`, `url` (YouTube, Vimeo or https .mp4/.webm), `imageUrl` (poster for a file) | | `tabs` | `title`, `body`, `layout` (`pills`\|`vertical`), `items[]` of `{ label, title, body, imageUrl, steps }` — `steps` is a list of job titles drawn as a career ladder | | `timeline` | `title`, `body`, `layout` (`vertical`\|`horizontal`\|`cards`), `items[]` of `{ label, title, body, imageUrl }` — `label` is the date or year | | `logo-row` | `title`, `layout` (`row`\|`slider`), `logos[]` of `{ imageUrl, alt }` | | `faq` | `title`, `items[]` of `{ question, answer (markdown) }` | | `cta` | `title`, `body`, `ctaLabel`, `ctaHref`, `imageUrl` (background photo), `align` (`center`\|`left`) | | `divider` | `style` (`line`\|`space`), `spacing` (`sm`\|`md`\|`lg`), `width` (`narrow`\|`wide`\|`full`) | ### Live blocks These render data from the portal itself. | type | Portal | Fields | | - | - | - | | `jobs` | interview | The open positions. `title`, `tags` (job tags from the position editor; a job matches one of them, case-insensitive), `query` (comma-separated keywords matched in title, description or location), `limit` (1–100), `showSearch`, `showTagFilter` (chips to narrow by tag), `allLabel`, `emptyText`, `ctaLabel`, `ctaHref` | | `job-openings` | interview | Job cards of the classic home page. `discoverId` (discovery interview id — never invent) | | `public-avatar` | interview | The talking avatar. `avatarId` (never invent) | | `easy-apply` | interview | "How it works" cards of the AI interview. No fields. | | `blogs` | both | Latest posts. `title`, `body`, `tag` (a blog tag; empty = all), `limit` (1–24), `layout` (`grid`\|`list`), `showExcerpt`, `showDate`, `ctaLabel`, `ctaHref` | | `pricing` | coaching | The configured pricing table. `title` | | `footer` | both | Site footer, on the `_footer` page. `logoUrl`, `logoAlt`, `columns[]` of `{ title, links[] of { label, href } }`, `topLinks[]` and `bottomLinks[]` of `{ label, href }`, `note` (markdown, e.g. copyright). Leave `topLinks`, `bottomLinks` and `note` out to drop those rows. | Links (`href`, `ctaHref`) are portal paths (`/jobs`, `/about/our-history`, `/about#values`) or full https URLs. ## Theme CSS `theme.css` restyles the whole portal on top of the platform styles. It is saved per portal (**Theme CSS** in the pages list) and applies to every page, the header and the footer — and on the interview screens, to the header and footer only, so the interview itself keeps its own look. Write every rule under `.jm-site`, the wrapper of the portal and of the editor canvas: | Hook | Element | | - | - | | `.jm-site` | The portal root. Set fonts and CSS variables here. | | `.jm-nav` | Header and menu. | | `.jm-footer`, `.jm-site-footer` | Footer area and the `_footer` page's footer block. | | `.jm-block--` | Every block of a type, e.g. `.jm-block--hero`. | | `.jm-tone--` | Sections of a tone, e.g. `.jm-tone--dark`. | | `.jm-card--overlay`, `.jm-card--stacked`, `.jm-card--tile` | Cards by style. | | `.jm-button`, `.jm-tabs__tab`, `.jm-job`, … | Buttons, tabs, job rows. | | your `className` | Any class you put on a block. | Use `var(--accent)` for the brand colour instead of a hard-coded value, so [Branding](/cookbooks/portal-customisation/branding) stays the source of truth. The tone variables `--jm-tone-muted-bg`, `--jm-tone-dark-bg`, `--jm-tone-dark-fg`, `--jm-tone-accent-bg` and `--jm-tone-accent-fg` recolour the sections without touching the blocks. `@import` of Google Fonts and `@font-face` are allowed. If the header is dark, upload a light logo under **Branding → Logo on dark backgrounds** and show it from the theme: ```css theme={null} .jm-site .jm-nav .jm-logo--default:has(+ .jm-logo--dark) { display: none; } .jm-site .jm-nav .jm-logo--dark { display: block; } ``` ## Rules * One language entry per portal language, with the same blocks in each (translated). * Keep block `id`s stable between imports of the same page; they are how the editor tracks blocks. * Never invent ids (`avatarId`, `discoverId`) or links to pages the document does not create. * A page shown in the menu needs a short `title` — it is the menu label. * Put the site footer on `_footer`, not at the bottom of every page. * Import unpublished first, check the pages in the editor, then publish. # Pre-screen candidates Source: https://developer.jobmojito.com/cookbooks/pre-screen-candidates Score a résumé against a position before inviting the candidate to a full interview. Pre-screening scores a candidate's résumé (and optional form answers) against a position, returning an AI recommendation. It's two steps: define the pre-screening position, then submit candidates. | Step | Endpoint | MCP tool | | - | - | - | | Create/update a pre-screening position | `POST /pre-screening-create` | `upsert_pre_screening` | | Screen a candidate (plain text résumé) | `POST /job-interview-pre-screening-api-resume-text` | `pre_screen_resume_text` | | Screen a candidate (uploaded file) | `POST /job-interview-pre-screening-api-resume-binary` | `pre_screen_resume_binary` | ## 1. Create the pre-screening position ```bash cURL theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/pre-screening-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "position_name": "Senior Backend Engineer", "position_location": "Remote (EU)", "position_country_code": "DE", "mojito_language_code": "en", "type": "resume", "status": "active", "position_description": "Backend role: Python, async APIs, AWS.", "candidate_expectations": "5+ yrs Python, cloud, SQL." }' ``` When creating, `position_name`, `position_location`, `position_country_code`, and `mojito_language_code` are required. `type` is `resume`, `form`, or `resume_with_form`. To **update** an existing position, pass `update_position_def_set_id` instead and only the fields you want to change. The response returns `position_def_set_id` (use it to screen candidates) and `interview_pre_screening_id`. ### Configuring the screening rules What the screen actually checks is defined by two arrays on the request: **`assessment_rules`** (the checks that pass / flag / reject a candidate) and **`form_fields`** (the candidate-facing form). Both are stored as-is and **replaced wholesale on update** — always send the complete array, never a partial delta. A rule only screens candidates when its `type` is `"screening"`. A rule with `type: "required"` or `"optional"` (or a resume rule you enabled without any threshold) is collected in the form but never rejects or flags anyone. The two resume-scoring rules — `resume_match` and `resume_technical_experience` — are always enforced when present and take no `type`. #### `assessment_rules` — the checks An **array** of rule objects. Each rule has an `id` (which check), an optional `type` (`optional` | `required` | `screening`), an optional `action` for when a **screening** rule is not met (`mark_for_review` | `reject`, default `mark_for_review`), and id-specific keys: | `id` | Applies to `type` | Extra keys | Passes when | | - | - | - | - | | `form_nationality` | form | `country` (string\[] of ISO codes), `country_eu` (bool) | Candidate nationality is in `country` **or** (`country_eu` and it's an EU country) | | `form_residency` | form | `country`, `country_eu` | Candidate residency is in `country` **or** an EU country | | `form_languages` | form | `languages`: `[{ code, level }]` | Candidate knows every listed language at **at least** the given `level` | | `form_education` | form | `education` (level key) | Candidate education ≥ `education`. Use `form_education_by_country` to screen against a country-specific ladder | | `form_visa` | form | `visa` (string\[] of visa keys) | Candidate visa is in the list. Visa keys are country-specific (only some countries, e.g. `KW`, `TR`) | | `form_age` | form | `age_min`, `age_max` | Candidate age is within `[age_min, age_max]` | | `form_gender` | form | `gender` (string\[] of gender keys) | Candidate gender is in the list | | `form_cover_letter` | form | — | File upload only — collected, never screened | | `form_national_id` | form | — | File upload only — collected, never screened | | `resume_match` | resume | `resume_score_reject`, `resume_score_accept` (0–10) | AI match score ≥ `resume_score_accept` passes; between the two → mark for review; below `resume_score_reject` (when > 0) → reject | | `resume_technical_experience` | resume | `technical_experience_years` | AI-detected years of relevant experience ≥ the value | | `resume_education` | resume | `education` (level key) | AI-detected education ≥ `education` (or `resume_education_by_country`) | **Allowed keys:** * `education`: `primary school`, `high school no graduation`, `high school`, `vocational`, `bachelors`, `masters`, `doctorate` * language `level`: `beginner`, `intermediate`, `advanced`, `fluent` * `gender`: `male`, `female`, `other` `form_age` and `form_gender` are unavailable (and ignored) for positions in EU / GDPR countries. `form_visa` is only available where visa keys are defined for the position country. #### `form_fields` — the candidate form An **array** describing what the candidate fills in. Two kinds of entry: * **Standard field reference** — `{ "id": "form_education", "mandatory": true }`. Mirrors a standard `assessment_rules` entry so the field is shown in the form (`mandatory` = the rule's `type` is not `optional`). Enable a standard form field in **both** arrays. * **Custom field** — `{ "id": "custom_", "title": "...", "type": "required", "field_type": "text" }`. A free-form question you add. `field_type` is `text` (free text), `file` (upload) or `radio` (single choice — supply `options: ["...", "..."]`, at least two). Custom fields are collected but **not** evaluated by the screening engine. `resume`-only positions ignore `form_fields`; `form` and `resume_with_form` positions require at least one entry. #### Full form + resume example ```bash Form + Resume position theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/pre-screening-create \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "position_name": "Customer Support Specialist", "position_location": "Remote", "position_country_code": "US", "mojito_language_code": "en", "type": "resume_with_form", "status": "active", "assessment_rules": [ { "id": "form_education", "type": "screening", "action": "reject", "education": "bachelors" }, { "id": "form_languages", "type": "screening", "action": "mark_for_review", "languages": [{ "code": "en", "level": "advanced" }] }, { "id": "form_nationality","type": "required" }, { "id": "resume_match", "resume_score_reject": 2, "resume_score_accept": 7 }, { "id": "resume_technical_experience", "action": "mark_for_review", "technical_experience_years": 3 } ], "form_fields": [ { "id": "form_education", "mandatory": true }, { "id": "form_languages", "mandatory": true }, { "id": "form_nationality", "mandatory": true }, { "id": "custom_1772004521633", "title": "Do you hold a valid work permit?", "type": "required", "field_type": "radio", "options": ["Yes", "No"] } ] }' ``` In this example a candidate below a bachelor's degree is **rejected**; one who lacks advanced English, has fewer than 3 years of experience, or scores 2–6 on the AI résumé match is **marked for review**; nationality and the work-permit question are collected but not screened. ## 2. Screen a candidate Submit the candidate against the `position_id` (the `position_def_set_id` from step 1). Required: `position_id`, `candidate_name`, `candidate_email`, `candidate_resume`. `candidate_country_code` is **optional**. It's used only as the fallback for `residency`/`nationality` when those aren't given in `form`. If the position has a `form_nationality` or `form_residency` **screening** rule and you send neither `candidate_country_code` nor the matching `form.nationality` / `form.residency`, the candidate is screened with no nationality/residency and **fails that rule** — marked for review or rejected per the rule's `action`. Supply the country code (or the `form` value) whenever such a rule is configured. ```bash Plain-text résumé theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-pre-screening-api-resume-text \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "position_id": "POSITION_DEF_SET_UUID", "candidate_name": "Peter Parker", "candidate_email": "peter@parker.com", "candidate_country_code": "US", "candidate_resume": "Full plaintext résumé here…", "candidate_linkedin_url": "https://linkedin.com/in/peterparker", "form": { "education": "masters", "languages": [{ "code": "en", "level": "fluent" }], "residency": "US" } }' ``` The `form` values must use the same keys the position's rules expect — `education` and language `level` from the lists above, ISO country codes for `residency`/`nationality`, and the position's `visa`/`gender` keys. Custom fields (defined via `form_fields`) can also be passed here by their `custom_` id; they're stored with the submission but not screened. For a binary file (PDF/DOCX), use `job-interview-pre-screening-api-resume-binary` with the same identity fields and the file payload instead of `candidate_resume`. ### Reading the result A completed screen returns: If the candidate isn't in a screenable state, you'll still get HTTP 200 with a short `{ "decision_status": …, "message": … }` payload instead of a full result. ## Shortcut: pre-screen + interview in one call To screen a candidate **and** get a ready interview link back in a single request, use [`POST /job-interview-create-for-candidate-with-token`](/cookbooks/create-an-interview#option-c-one-call-per-candidate-returns-a-link) (HTTP only — not exposed as an MCP tool; call it directly with a bearer token). It resolves the position, runs pre-screening, and returns a tokenised `interview_url` plus a `status` like `ai_accept` or `recruiter_action`. ## Next steps Invite the candidates who passed screening. Filter results by the `pre-screening` step to see screening outcomes. # Review results Source: https://developer.jobmojito.com/cookbooks/review-results List completed interviews, read full transcripts and AI assessments, and export reports. Reviewing a candidate is three steps: find the result, read its details, and (optionally) export a report. | Step | Endpoint | MCP tool | | - | - | - | | List results | `GET /merchant-result-list` | `list_interview_results` | | Read transcript + scores | `POST /job-interview-details` | `get_interview_result_details` | | Export PDF / HTML / JSON | `POST /job-interview-pdf` | `generate_interview_report` | ## 1. Find the result List your merchant's results, newest first. Everything is a query parameter — no request body. ```bash cURL theme={null} curl -G https://cool.jobmojito.com/functions/v1/merchant-result-list \ -H "Authorization: Bearer $SUPABASE_JWT" \ --data-urlencode "tab=completed" \ --data-urlencode "order_by=score" \ --data-urlencode "limit=25" ``` Useful filters: `interview_id` (one interview), `tab` (`completed`, `shortlist`, `undecided`, `decided-selected`, `decided-rejected`, …), `step` (`pre-screening` or `interview`), `order_by` (`score`, `created_at_newest`, …), `filter_text`, plus `limit`/`offset`. The response has `data[]` and `pagination`. Each row includes `interview_result_id`, `candidate_name`, `candidate_email`, `status`, `decision_status`, and `score`. Grab the `interview_result_id` for the next step. [Interview role-plays](/cookbooks/create-an-interview) appear in this list too, carrying `type: "persona_interview"`. They are scored on the persona scoring path — you get per-answer and session-level feedback rather than a question-by-question rubric — but the listing, filtering, and detail endpoints work the same way. To browse candidates instead of results, use `GET /merchant-candidate-list` (`list_candidates`); to browse interview definitions, use `GET /merchant-interview-list` (`list_interviews`). ## 2. Read the transcript and assessment ```bash cURL theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-details \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "interview_result_id": "RESULT_UUID", "get_signed_recordings": false }' ``` You get the overall scores (`score`, `score_sentiment`, `score_words_per_minute`, …), AI write-ups (`ai_analysis`, `ai_analysis_recruiter`, and `ai_analysis_other.strengths_concerns` with its `strengths` / `concerns` lists — the older `ai_analysis_recruiter_why_hire` / `…_why_not_hire` fields are deprecated copies of those), any `recruiter_risks`, and the full `transcript` array. Each transcript item has `question_asked`, `answer`, per-answer `ai_analysis`, and a `score`. ## 3. Export a report Generate a shareable report as a PDF (signed URL), raw HTML, or structured JSON. Pass a single `interview_result_id` or an array `interview_result_ids` for a combined report. ```bash PDF theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-pdf \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "interview_result_id": "RESULT_UUID", "export_type": "pdf", "store_file": true, "export_features_result": { "ai_recruiter_assessment": true, "ai_scoring_rubric": true, "transcript": true, "analytics": true } }' ``` ```bash JSON theme={null} curl -X POST https://cool.jobmojito.com/functions/v1/job-interview-pdf \ -H "Authorization: Bearer $SUPABASE_JWT" \ -H "Content-Type: application/json" \ -d '{ "interview_result_id": "RESULT_UUID", "export_type": "json" }' ``` The response shape depends on `export_type`: * `pdf` → `pdf_export_url` + `pdf_export_valid_until` (set `store_file: true`). * `html` → `html_export` (a full HTML document string). * `json` → `json_export` (structured object). ### Report layout (`template`) Optional, defaults to `classic`: * `classic` — the original card-based report. * `modern` — the full report on a white page with hairline rules, in the visual style of the recruiter result page. * `one_pager` — a single summary page with a status rail: score, verdict, AI feedback and hiring reasons. No transcript or analytics; uploaded documents still follow as extra pages when `files` is on. `template` may also be set inside `export_features_result` (that is how a saved auto-export configuration carries it); the top-level field wins when both are given. ### Report content toggles (`export_features_result`) Set `export_features_result.mojito_language_code` to translate the report into another language (0.1 credit per result when it differs from the result's own language). Want to give a candidate another go? `POST /job-interview-result-request-another-attempt` (`request_another_interview_attempt`) re-opens a submitted result. On a [interview role-play](/cookbooks/create-an-interview) this works even once the candidate has used up their attempts — a released attempt no longer counts against the cap. # Interview and pre-screening workflows Source: https://developer.jobmojito.com/cookbooks/workflows # API use: interview and pre-screening workflows Everything you can do from the JobMojito admin, you can also drive from your own backend: create interviews and pre-screening positions, register or invite candidates, and read results, all over plain HTTPS `POST`/`GET` calls to `https://cool.jobmojito.com/functions/v1`, authenticated with a Supabase JWT bearer token. The whole surface is described by an OpenAPI 3.1 document, which also makes it MCP-friendly if you'd rather drive it from an agent than hand-roll HTTP calls. The shape of each flow below matches direct use. The only structural differences are *who* creates and publishes the interview (your code, via API calls, instead of a recruiter in the admin) and *how* results come back (a webhook delivered to your endpoint, instead of only appearing in the admin UI). One flow, synchronous pre-screening, has no direct-use equivalent at all, because it skips the candidate portal entirely. A note on webhooks before the flows: JobMojito fires three events: `interview_collected` (raw answers, before scoring), `interview_result` (final score and AI analysis, delayed until the PDF is ready if you've enabled report generation), and `interview_failed` (abandoned, no speech detected, or otherwise never completed). Delivery is at-least-once with up to 10 retries over 60-second timeouts, and `interview_result` re-fires whenever a watched field changes later, including a recruiter's decision, so your handler needs to upsert by `id` and treat every delivery as idempotent, not assume single delivery or strict ordering. Separately from the webhook, JobMojito also emails the candidate directly whenever a session is abandoned or fails, so they can continue or retake it without you having to prompt them. ## Interview 1. **Create the interview:** `POST /job-interview-create` (AI-generated questions from a job description) or `POST /job-interview-create-from-array` (your own question list). Either call returns an `interview_def_set_id` you'll reuse; this same id shows up elsewhere under different field names (`interview_id`, `position_id`), so keep the [identifiers reference](https://developer.jobmojito.com/mcp/identifiers) handy. To change the questions afterwards, read them back with `GET /job-interview-get` and send the edited array to `POST /job-interview-update` — it is diffed against what is stored, so the questions you left alone keep their existing records; see [Edit interview questions](https://developer.jobmojito.com/cookbooks/edit-interview-questions). 2. **Get candidates in:** `POST /job-interview-register-users` with `type: "url"` to get signed links back for you to deliver yourself, or `type: "invitation"` to have JobMojito send and manage the email chain (invite, reminders, completion notice). `POST /invite-users` is the alternative when you're inviting people to the merchant more broadly. Both are batch endpoints: every request returns HTTP 200 with a per-row `result`, so a bad email in row 3 doesn't fail rows 1 and 2; inspect each row rather than trusting the top-level status. 3. **Candidate authenticates, confirms their profile, and takes the AI interview:** identical to direct use. 4. **If the candidate abandons or fails the session,** JobMojito fires `interview_failed` to your endpoint and separately emails the candidate so they can continue or retake it; scoring fields are typically empty on that webhook. 5. **On submission,** JobMojito fires `interview_collected` immediately (raw answers), then scores the interview and fires `interview_result` to your configured endpoint, held back until the PDF report is attached, if you've turned that on. 6. **A human still makes the call.** A reviewer records Select/Reject in JobMojito's Pending decisions screen; that decision change re-fires `interview_result` so your system picks it up without polling. ## Pre-screening with interview 1. **Define the screening rules:** `POST /pre-screening-create` with `type: "resume" | "form" | "resume_with_form"`, plus `assessment_rules` (what actually screens: nationality, residency, languages, education, visa, age, gender, resume match score, detected experience) and `form_fields` (what the candidate sees). Both arrays are replaced wholesale on every update, so always send the complete array. 2. **Define the interview stage:** `POST /job-interview-create`, same as the plain Interview flow. 3. **Get the candidate in,** the same way as above, or use the shortcut, `POST /job-interview-create-for-candidate-with-token`, which resolves the position, runs pre-screening, and returns a ready `interview_url` plus a `status` (`ai_accept`, `ai_reject`, or `recruiter_action`) in one call per candidate. 4. **Candidate authenticates and completes the pre-screening form/resume upload.** 5. **JobMojito scores it against your rules,** landing the candidate in one of three bands, identical logic to direct and ATS use: * Above the `resume_score_accept` threshold (or a passing form band), non-EU: auto-accepted into the interview. * Below `resume_score_reject`, non-EU: auto-rejected, visible via `GET /job-interview-details`; no webhook fires for pre-screening itself. * Mid-band (a rule's `action: "mark_for_review"`), or an EU/GDPR candidate: routed to a recruiter's Pending decisions screen regardless of score. 6. **If accepted, the candidate takes the interview** (with the same abandoned/failed email and continue-or-retake option, and `interview_failed` webhook, as the plain Interview flow), and results otherwise follow the same webhook path: `interview_collected`, then scoring, then `interview_result` to your endpoint, and a human Select/Reject re-fires the event. ## Pre-screening only (synchronous) This is the one API-only shape with nothing else like it in direct or ATS use: there's no candidate portal and no interview at all. You already have the resume; you send it, and the score comes back in the same HTTP response. 1. **Define the position once:** `POST /pre-screening-create`, same rules/form\_fields shape as above. 2. **Screen each candidate:** `POST /job-interview-pre-screening-api-resume-text` (plaintext resume) or `POST /job-interview-pre-screening-api-resume-binary` (uploaded file), with `position_id`, `candidate_name`, `candidate_email`, and the resume. Optional `form` values (education, languages, residency, nationality, visa, age, gender) get matched against whichever rules you configured. 3. **The response returns synchronously,** no webhook involved, with `success`, `decision_status`, and a `recommendation` of `ai_accept`, `ai_reject`, or `recruiter_action`, plus `pre_screening_score`, `resume_ai_analysis`, `resume_ai_education`, `resume_ai_technical_experience`, and (when relevant) `processing_reason`. 4. **Act on `recommendation` yourself.** `ai_accept` and `ai_reject` are automated outcomes available for non-EU positions only: advance the candidate in your own system on `ai_accept`, or reject/archive on `ai_reject`. `recruiter_action` is what you get instead for EU/GDPR positions (and mid-band non-EU candidates): flag the candidate for human review in JobMojito's Pending decisions rather than deciding automatically. If the candidate isn't in a screenable state at all, you still get HTTP 200, just a short `{ decision_status, message }` object instead of the full result, rather than an error. # How credits work Source: https://developer.jobmojito.com/how-credits-work JobMojito runs on consumption-based credits. See what each interview, add-on and pre-screen costs, and how billing works. JobMojito is **consumption-based**: you hold a balance of **credits** and each action you run deducts from it. There's no per-API-call fee - you're billed for the work performed (an interview taken, a candidate pre-screened, and so on). New accounts start with **10 free credits**, no card required. > Credit *costs* (below) are stable and feature-based. The *price per credit* depends on your plan or top-up bundle. For current prices see [jobmojito.com/interview/pricing](https://jobmojito.com/interview/pricing). ## What an interview costs An interview's cost is computed by the billing engine as: > **(base format cost + add-ons) × duration multiplier** Assessments are billed the same way as interviews. So are **interview role-plays** — the interview-portal variant of a [role-play persona](/cookbooks/create-an-interview), where the avatar plays a defined role instead of asking a scored question list. Although the session is free-form, it is billed exactly like a standard interview: the same base format cost, the same recording add-ons, and the same duration multiplier, deducted from your **merchant** credit balance. Each **completed** session is billed — credits are deducted when the session completes, not when it starts. Interview role-plays allow repeat attempts (`interview_attempts`, default 3), so budget for up to that many charged sessions per candidate: a 3-credit role-play with the default cap can cost up to 9 credits if a candidate uses every attempt. > The **coaching persona** is the other variant and is billed differently: it runs on the coaching portal against the consumer's coaching credits, not your merchant credits. ### Base format cost The base cost depends on how the interview is delivered: | Format | Credits | When it applies | | - | - | - | | Avatar, non-interactive | 0.35 | Non-interactive avatar interview. | | Voice only | 1 | The candidate uses the voice-only experience. | | Avatar + voice, early stop | 1.5 | Interactive avatar interview with early-stop scoring enabled. | | Avatar + voice | 2.5 | Interactive avatar interview (full). | ### Add-ons Charged **on top** of the base cost when enabled. They map directly to fields on the create endpoints (see [How the API works](/how-the-api-works)): | Add-on | API field | Extra credits | | - | - | - | | Cheating detection - audio | `recording: "audio_all"` / `"audio_first_5_answers"` | +0.1 | | Cheating detection - audio & video | `recording: "video_all"` / `"video_first_5_answers"` | +0.25 | | Full-session recording - audio | `recording_full_session: "audio_all"` | +0.2 | | Full-session recording - video | `recording_full_session: "video_all"` | +0.4 | ### Duration multiplier The base + add-ons subtotal is then **multiplied** by a factor based on the interview's configured maximum duration (the `max_duration` field, in seconds, set when you create the interview): | Configured max duration | Multiplier | | - | - | | ≤ 20 min (≤ 1200s) | ×1 | | ≤ 30 min (≤ 1800s) | ×2 | | ≤ 45 min (≤ 2700s) | ×3 | | ≤ 60 min (≤ 3600s) | ×4 | | > 60 min (> 3600s) | ×5 | > The multiplier uses the **configured** maximum length, not how long the candidate actually took - a longer allowed interview costs proportionally more. The single-interview prices on the pricing page assume the ×1 (≤ 20 min) tier. ### Worked example An interactive `Avatar + voice` interview (2.5) with video full-session recording (`recording_full_session: "video_all"`, +0.4) and audio + video cheating detection (`recording: "video_all"`, +0.25), configured for a 30-minute max duration (×2): ```text theme={null} (2.5 + 0.4 + 0.25) × 2 = 6.3 credits ``` At a 20-minute max duration the same interview costs `3.15 × 1 = 3.15 credits`. ## Pre-screening Resume pre-screening (`/job-interview-pre-screening-api-resume-text` and `/job-interview-pre-screening-api-resume-binary`) costs **0.10 credits per candidate**. ## Report translation Generating an interview report (`/job-interview-pdf`) in the result's own language is included. Passing a `mojito_language_code` to **translate** the report into another language costs an extra **0.1 credits**. ## Plans & top-ups You can run on a monthly subscription (which includes a credit allowance) and/or top up credits at any time. Current options (see the [pricing page](https://jobmojito.com/interview/pricing) for live numbers): **Monthly plans** | Plan | Price / month | Included credits | Seats | | - | - | - | - | | Starter | €79 (€39 first month) | 30 | 1 | | Growth | €249 (credits doubled first month) | 100 | 5 | | Scale | €599 | 270 | 15+ | **Credit top-up bundles** | Bundle | Price | Credits | Per credit | | - | - | - | - | | Starter | €300 | 150 | €2.00 | | Volume | €2,900 | 1,700 | €1.70 | | Scale | €9,900 | 6,600 | €1.50 | Top-ups are available anytime from €1.50/credit. A custom avatar is available as an optional add-on (€1,490/year). ## Billing model * Credits are deducted when an action runs (an interview is taken, a candidate is pre-screened, a report is translated). * Your current balance and usage are visible in the JobMojito admin. * No annual lock-in - top up as you go. If a call would exceed your available balance, the action is rejected - keep an eye on your balance (or enable auto top-up) for high-volume integrations. # How the API works Source: https://developer.jobmojito.com/how-the-api-works Base URL, request and response format, error model, credits, and the full list of JobMojito API endpoints. The JobMojito API is a set of HTTPS endpoints. Calls are simple: **POST** a JSON body, get a JSON response. ## Base URL ```text theme={null} https://cool.jobmojito.com/functions/v1 ``` A full endpoint URL is the base URL plus the endpoint path, e.g. `https://cool.jobmojito.com/functions/v1/job-interview-create`. ## Conventions * **Method:** actions that create or change something use `POST`; read-only lookups and lists use `GET` (with parameters in the query string). * **Auth:** every request needs `Authorization: Bearer ` - see [Authentication](/authentication). * **Request body:** `application/json` (one endpoint, the binary resume upload, uses `multipart/form-data`). * **Response body:** `application/json`. * **IDs:** resources are identified by UUIDs. ```bash theme={null} curl https://cool.jobmojito.com/functions/v1/job-interview-create \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Project manager", "location": "remote", "interview_template_id": "…", "mojito_language_code": "en", "status": "active", "type": "interview", "visibility": "merchant_public" }' ``` ## Environments and testing Each **interview** has an `environment`: `production` (the default), `uat`, `development` or `demo`. You set it in the admin app (**Interview ▸ Edit ▸ Custom integration ▸ Environment**), or with the `environment` field on `job-interview-update`. Each webhook is also registered for one environment. When a result fires an event, it goes **only to the webhooks whose environment matches the environment of that interview**. Your API token plays no part in this routing. A typical test set-up: 1. Create the test interview with `environment: "uat"`. 2. Register a `uat` webhook that points at your test system (for example your ATS sandbox). 3. Invite test candidates with `is_test: true` on [`job-interview-register-users`](/cookbooks/invite-candidates). Test links work on draft interviews too, and their results are flagged as tests. Test results go only to the test endpoint, and production webhooks never see them. Test interviews use credits like any other interview. For a fully separate set of users, interviews, API keys and webhooks, ask your account contact for a **UAT sub-merchant** next to your production merchant. ## Errors When a request fails, you get a non-2xx status and a JSON error envelope: ```json theme={null} { "error": "Field is required.", "name": "interview_template_id" } ``` * `error` - a human-readable message. * `name` - present for field-level validation problems; the name of the offending field. ### Status codes | Status | Meaning | | - | - | | `200` | Success. | | `401` | Missing or invalid authentication. | | `403` | Authenticated but not permitted. | | `404` | Resource not found. | | `409` | Conflict - the request can't be applied in the current state. | | `422` | Validation error (a field is missing or invalid). | | `500` | Unexpected server error. | ### Batch endpoints return per-row results Endpoints that act on a list - `invite-users` and `job-interview-register-users` - process each row independently and **always return HTTP 200** with a results array. A bad row doesn't fail the whole request; instead that row carries an error while the others succeed: ```json theme={null} [ { "email": "ok@example.com", "result": "ok", "interview_url": "https://…" }, { "email": "bad", "result": "Error: Email address is not valid" } ] ``` Always inspect each row's `result` field rather than relying on the HTTP status alone. ## Credits Some actions consume your account's credits - for example creating an interview, running a pre-screen, or generating a PDF report. A few options add cost on top (e.g. translating a report to another language, or enabling full-session recording). Your current balance and usage are visible in the admin. See [How credits work](/how-credits-work) for per-action costs, plans and top-ups. ## OpenAPI specification & MCP The entire API is described by a machine-readable **OpenAPI 3.1** document, which powers this reference and can be imported into your own tooling: ```text theme={null} https://cool.jobmojito.com/functions/v1/openapi ``` Webhooks have their own spec: ```text theme={null} https://cool.jobmojito.com/functions/v1/openapi-webhooks ``` Because the spec is OpenAPI, the API is also **MCP-friendly** - AI agents can discover and call the endpoints from the same definition. Each endpoint and field includes a description to guide automated use. ## Endpoints ### Interviews & assessments | Endpoint | Purpose | | - | - | | `POST /job-interview-create` | Create a new interview (AI-generated questions from a job description). | | `POST /job-interview-create-from-array` | Create an interview from your own array of questions. | | `POST /job-interview-create-for-candidate-with-token` | Create an interview for a candidate and get a ready-to-use access URL. | | `GET /job-interview-get` | Fetch an interview definition, including its ordered `questions` array. | | `POST /job-interview-update` | Update an existing interview / position — settings, and the question list when you send `questions`. | | `POST /job-interview-set-state` | Change an interview's lifecycle state and/or manage its iframe embed key. | | `POST /job-interview-token` | Generate a signed interview URL. | | `POST /job-interview-result-request-another-attempt` | Allow a candidate another attempt. | ### Coaching catalogue | Endpoint | Purpose | | - | - | | `GET /catalogue-tag-list` | List catalogue directories (search, filter, or walk the tree with `parent_tag`). | | `GET /catalogue-tag-get` | Read one directory in full, with its content page and the sessions it lists. | | `POST /catalogue-tag-create` | Create a directory (page) in the coaching portal catalogue. | | `POST /catalogue-tag-update` | Update a catalogue directory, including its custom Markdown content page. | A directory lists a coaching session when the session's `tags` contain every tag in the directory's `tags_interview_set_filter` — so `tags` on the interview and `tags_interview_set_filter` on the directory are the two halves of the same mapping. ### Candidates & invitations | Endpoint | Purpose | | - | - | | `POST /invite-users` | Invite users / candidates (batch, per-row results). | | `POST /job-interview-register-users` | Register candidates for an interview and get their links (batch, per-row results). | ### Results & reports | Endpoint | Purpose | | - | - | | `GET /job-interview-details` | Get an interview result with full transcript and AI analysis. | | `POST /job-interview-pdf` | Generate a branded PDF / HTML / JSON interview report. | ### Pre-screening | Endpoint | Purpose | | - | - | | `POST /pre-screening-create` | Create or update a pre-screening position. | | `POST /job-interview-pre-screening-api-resume-text` | Pre-screen a candidate from a plaintext resume. | | `POST /job-interview-pre-screening-api-resume-binary` | Pre-screen a candidate from an uploaded resume file. | ### Knowledge base | Endpoint | Purpose | | - | - | | `POST /knowledge-base-document-upload` | Upload a document the AI can draw questions/context from. | ### Merchant reads (lists & status) Read-only endpoints scoped to the merchant from your token (or a `merchant_id` query parameter). Being reads, these use `GET`. | Endpoint | Purpose | | - | - | | `GET /merchant-interview-list` | List the merchant's interview definitions. | | `GET /merchant-candidate-list` | List candidates. | | `GET /merchant-result-list` | List interview results. | | `GET /merchant-avatar-list` | List available interviewer avatars. | | `GET /merchant-sub-merchant-list` | List sub-merchants you manage. | | `GET /merchant-analytics` | Daily event analytics. | | `GET /merchant-status` | Status snapshot: credit balances, subscription, pending-work counts, candidate/result totals, invitation headroom. | See the API Reference for each endpoint's exact request and response schema, or fetch the [OpenAPI spec](https://cool.jobmojito.com/functions/v1/openapi). # Connect Claude Code Source: https://developer.jobmojito.com/mcp/connect/claude-code Add the JobMojito MCP server to Claude Code (CLI) and authorize it over OAuth. Connect [Claude Code](https://claude.com/claude-code) to JobMojito so it can create interviews, invite candidates, and pull results from the terminal. **Endpoint:** `https://mcp.jobmojito.com/mcp` · **Auth:** OAuth (no API key). Register the remote server (user scope makes it available in every project): ```bash theme={null} claude mcp add --transport http --scope user jobmojito https://mcp.jobmojito.com/mcp ``` Start Claude Code and run `/mcp`, select **jobmojito**, and choose to authenticate. Your browser opens the JobMojito sign-in; approve access and return to the terminal. `claude mcp list` should show `jobmojito` as connected, and the JobMojito tools become available. Try: *"List my JobMojito interviews."* If a tool later returns a `401` / `invalid_token`, the session isn't authorized — run `/mcp` and reconnect. ## Next steps * [Identifiers & admin links](/mcp/identifiers) — get the ids right. * [Tools reference](/mcp/tools) — the full tool inventory. # Connect Claude Desktop Source: https://developer.jobmojito.com/mcp/connect/claude-desktop Add JobMojito as a custom connector in the Claude desktop app and authorize it over OAuth. Add JobMojito to the [Claude desktop app](https://claude.ai/download) as a custom connector so Claude can run your hiring workflows in chat. **Endpoint:** `https://mcp.jobmojito.com/mcp` · **Auth:** OAuth (no API key). In Claude, go to **Settings → Connectors**. Choose **Add custom connector**, set the name to `JobMojito`, paste the endpoint URL `https://mcp.jobmojito.com/mcp`, and add it. Click **Connect** on the JobMojito connector and complete the JobMojito sign-in in the browser window that opens. Approve access to finish. Enable the connector in a chat and ask, e.g. *"List my JobMojito interviews."* Custom connectors require a Claude plan that supports them. This is the same Connectors flow used on the web — see [Connect Claude Web](/mcp/connect/claude-web). ## Next steps * [Identifiers & admin links](/mcp/identifiers) — get the ids right. * [Tools reference](/mcp/tools) — the full tool inventory. # Connect Claude (web) Source: https://developer.jobmojito.com/mcp/connect/claude-web Add JobMojito as a custom connector on claude.ai and authorize it over OAuth. Add JobMojito to [claude.ai](https://claude.ai) as a custom connector so Claude on the web can create interviews, invite candidates, and review results. **Endpoint:** `https://mcp.jobmojito.com/mcp` · **Auth:** OAuth (no API key). On claude.ai, go to **Settings → Connectors**. Choose **Add custom connector**, name it `JobMojito`, and paste the endpoint URL `https://mcp.jobmojito.com/mcp`. Click **Connect** and complete the JobMojito sign-in, then approve access. Turn on the connector for a conversation and try *"List my JobMojito interviews."* Custom connectors require a claude.ai plan that supports them (e.g. Pro/Team/ Enterprise). The desktop app uses the same flow — see [Connect Claude Desktop](/mcp/connect/claude-desktop). ## Next steps * [Identifiers & admin links](/mcp/identifiers) — get the ids right. * [Tools reference](/mcp/tools) — the full tool inventory. # Connect Cursor Source: https://developer.jobmojito.com/mcp/connect/cursor Add the JobMojito MCP server to Cursor via mcp.json and authorize it over OAuth. Add JobMojito to [Cursor](https://cursor.com) so the agent can drive your hiring workflows from the editor. **Endpoint:** `https://mcp.jobmojito.com/mcp` · **Auth:** OAuth (no API key). Edit `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (this project): ```json theme={null} { "mcpServers": { "jobmojito": { "url": "https://mcp.jobmojito.com/mcp" } } } ``` Open **Settings → MCP** (or **Cursor Settings → Tools & Integrations**). The `jobmojito` server appears — click it to sign in and approve access via OAuth. In the Agent, ask e.g. *"List my JobMojito interviews."* The JobMojito tools show under the server once connected. ## Next steps * [Identifiers & admin links](/mcp/identifiers) — get the ids right. * [Tools reference](/mcp/tools) — the full tool inventory. # Connect a custom agent (MCP SDK) Source: https://developer.jobmojito.com/mcp/connect/custom-agent Connect any MCP-capable client or custom agent to JobMojito using an MCP SDK over Streamable HTTP with OAuth. Any MCP-capable client works with JobMojito — if your platform isn't listed under **Connect**, wire it up with an MCP SDK. **Endpoint:** `https://mcp.jobmojito.com/mcp` · **Transport:** Streamable HTTP · **Auth:** OAuth 2.0 (no API key). ## What your client needs 1. **Streamable HTTP transport** pointed at `https://mcp.jobmojito.com/mcp`. 2. **An OAuth handler.** The server is an OAuth resource server; the authorization server is discovered from `https://mcp.jobmojito.com/.well-known/oauth-protected-resource/mcp`. Run the standard authorization-code flow, then send the access token as a `Authorization: Bearer ` header on every request. Persist the refresh token so the agent can reconnect without a fresh login each run. 3. **Retry on `401` / `invalid_token`** by re-running the OAuth flow. Most MCP SDKs (e.g. the Python `mcp` package or the TypeScript `@modelcontextprotocol/sdk`) provide a Streamable HTTP client and an OAuth helper, so you point them at the endpoint and let the SDK manage discovery, tokens, and refresh. Prefer the interactive OAuth flow over hand-managing tokens. If you must inject a token yourself (e.g. a headless backend), obtain it from the JobMojito OAuth flow and pass it in the `Authorization` header — never hard-code long-lived secrets. ## Not building an agent? For server-to-server automation you often want the [HTTP API](/how-the-api-works) directly with a bearer token, rather than MCP. ## Next steps * [Identifiers & admin links](/mcp/identifiers) — get the ids right. * [Tools reference](/mcp/tools) — the full tool inventory. * [Skills](/skills/overview) — reusable patterns for reliable agent workflows. # Connect Gemini CLI Source: https://developer.jobmojito.com/mcp/connect/gemini-cli Add the JobMojito MCP server to the Gemini CLI and authorize it over OAuth. Add JobMojito to the [Gemini CLI](https://github.com/google-gemini/gemini-cli) so it can run your hiring workflows from the terminal. **Endpoint:** `https://mcp.jobmojito.com/mcp` · **Auth:** OAuth (no API key). Add JobMojito under `mcpServers` in `~/.gemini/settings.json` (global) or `.gemini/settings.json` (this project): ```json theme={null} { "mcpServers": { "jobmojito": { "httpUrl": "https://mcp.jobmojito.com/mcp" } } } ``` In the Gemini CLI, run `/mcp` to list servers, select **jobmojito**, and complete the JobMojito OAuth sign-in in the browser. Ask e.g. *"List my JobMojito interviews."* Run `/mcp` again anytime to check connection status. ## Next steps * [Identifiers & admin links](/mcp/identifiers) — get the ids right. * [Tools reference](/mcp/tools) — the full tool inventory. # Connect ChatGPT & OpenAI Source: https://developer.jobmojito.com/mcp/connect/openai Use JobMojito as a connector in ChatGPT, or as a remote MCP tool from the OpenAI Responses API. Connect JobMojito to OpenAI two ways: as a **connector in ChatGPT**, or as a **remote MCP tool** from the OpenAI API. **Endpoint:** `https://mcp.jobmojito.com/mcp` · **Auth:** OAuth (no API key). ## ChatGPT (connector) In ChatGPT, go to **Settings → Connectors** and open the custom / advanced connector option. Add a custom connector with the URL `https://mcp.jobmojito.com/mcp`. Complete the JobMojito sign-in to authorize, then enable the connector in a chat and try *"List my JobMojito interviews."* If you don't see a custom-connector option, enable **Developer mode** under **Settings → Security and login**, then add the server from the plugins/connectors screen. Developer mode is the standard route for a server that isn't yet listed in OpenAI's directory, and it supports both read and write tools. Availability varies by plan, and on Business/Enterprise/Edu workspaces an admin may need to enable it first. Menu names change over time — look for "Connectors", "Plugins" and "custom". ## OpenAI API (Responses, remote MCP) The [Responses API](https://platform.openai.com/docs) can call a remote MCP server directly. Because JobMojito is OAuth-protected, you must pass a valid **JobMojito access token** (obtained from the OAuth flow) in the `Authorization` header — the API path does not run the interactive login for you. ```python theme={null} from openai import OpenAI client = OpenAI() resp = client.responses.create( model="gpt-5", tools=[{ "type": "mcp", "server_label": "jobmojito", "server_url": "https://mcp.jobmojito.com/mcp", "headers": {"Authorization": "Bearer "}, "require_approval": "always", }], input="List my JobMojito interviews.", ) print(resp.output_text) ``` ## Next steps * [Identifiers & admin links](/mcp/identifiers) — get the ids right. * [Tools reference](/mcp/tools) — the full tool inventory. # Connect VS Code Source: https://developer.jobmojito.com/mcp/connect/vscode Add the JobMojito MCP server to VS Code (Copilot agent mode) and authorize it over OAuth. Add JobMojito to [VS Code](https://code.visualstudio.com) so Copilot's agent mode can create interviews, invite candidates, and review results. **Endpoint:** `https://mcp.jobmojito.com/mcp` · **Auth:** OAuth (no API key). Create `.vscode/mcp.json` in your workspace (or add to your user settings): ```json theme={null} { "servers": { "jobmojito": { "type": "http", "url": "https://mcp.jobmojito.com/mcp" } } } ``` You can also run **MCP: Add Server** from the Command Palette and choose an HTTP server. Start the server from the `mcp.json` editor (**Start**) or via **MCP: List Servers**. VS Code opens the JobMojito OAuth sign-in; approve access. Open Chat in **Agent** mode and try *"List my JobMojito interviews."* The JobMojito tools appear in the tools picker. MCP support requires GitHub Copilot with agent mode enabled in VS Code. ## Next steps * [Identifiers & admin links](/mcp/identifiers) — get the ids right. * [Tools reference](/mcp/tools) — the full tool inventory. # Data & privacy Source: https://developer.jobmojito.com/mcp/data-and-privacy What the JobMojito MCP tools can reach, what is visible before sign-in, where candidate data goes, and how retention and logging work. Connecting an AI agent to JobMojito means an external model can read your hiring data. This page states exactly what it can reach, and what it can't. ## What requires sign-in Everything that touches your data. No tool can be called without a verified JobMojito access token, and each call executes **as the signed-in user**, so it respects that user's own permissions and merchant scope. What is available **without** signing in is capability metadata only: | Available anonymously | Requires a signed-in token | | - | - | | Tool names, descriptions and input schemas (`tools/list`) | Calling any tool | | Server capabilities and instructions (`initialize`) | Reading any resource | | A health check at `/healthz` | Rendering any prompt | This is deliberate. Publishing the tool list lets MCP directories and clients show what the server does before asking you to log in. It exposes the shape of the public [API](/how-the-api-works) — nothing about your account, your candidates or your interviews. ## What a connected agent can see An agent with a signed-in connection can read whatever the API would return to that user, which for hiring workflows includes personal data: * **Candidate records** — name, email, and any identifiers you supplied. * **Interview results** — full transcripts, per-question answers, AI analysis, scores. * **Interview definitions**, knowledge base documents and merchant analytics. That content is returned to the MCP client, which means it is processed by **whichever AI provider you connected** — Anthropic, OpenAI, Google, or your own model host — under that provider's terms, not JobMojito's. This is the single most important thing to understand before connecting a production account. Connect the account whose data you are willing to send to your chosen AI provider. If you handle candidate data under a DPA that restricts sub-processors, confirm the provider is covered before connecting, and prefer a scoped sub-merchant over a parent account that can see everything. ## Reducing exposure * **Scope with `merchant_id`.** Connect as a user who can only see the merchants they need — see [Merchant scoping](/mcp/overview#merchant-scoping). * **Ask for less.** The read-only `list_*` tools return summaries; call `get_interview_result_details` only when a full transcript is genuinely needed. * **Use a dedicated user.** An MCP-specific JobMojito user with narrow permissions makes the access boundary explicit and revocable. * **Revoke cleanly.** Removing the connector in your AI client ends its access; revoking the user in JobMojito ends it everywhere. ## Retention and data-subject rights Using MCP changes nothing here — the tools call the same API as any other integration. Candidate interview data is retained for **12 months by default** and is configurable by the customer; access and security logs are retained for 12 months. Defaults, sub-processors and the international-transfer position are in the [Privacy Policy](https://jobmojito.com/privacy_policy) and at [trust.jobmojito.com](https://trust.jobmojito.com). Your AI provider's retention of the conversation itself is separate and governed by them. ## What JobMojito logs about MCP use The server logs the **name of each tool called, its argument keys, and the outcome and duration** — for diagnostics and abuse prevention. It does not log argument values, tool results, or any conversation content. ## Security * HTTPS only, over MCP Streamable HTTP. * OAuth 2.1 with PKCE. Tokens are never accepted in a URL. * The server is an OAuth resource server and validates each token's signature, issuer, expiry and audience against the resource it advertises at `/.well-known/oauth-protected-resource/mcp`. * Report a suspected vulnerability to [**security@jobmojito.com**](mailto:security@jobmojito.com). ## Related * [Responsible use](/mcp/responsible-use) — human review and candidate disclosure. * [Authentication](/authentication) — how tokens map to environments and permissions. # Identifiers & admin links Source: https://developer.jobmojito.com/mcp/identifiers What each JobMojito id means, where to get it, why the same interview id has different field names on different endpoints, and how to build admin app links. Most mistakes when driving JobMojito come from **using the wrong id in the wrong field**. This page is the reference: what each id means, which tool/endpoint returns it, and which field expects it. When you're unsure which id to pass, come back here rather than guessing. The same interview lives under **three different field names** depending on the endpoint (`interview_def_set_id`, `position_id`, `interview_id`), and result endpoints need `interview_result_id` — **not** the result row's `id`. See [The interview id has three names](#the-interview-id-has-three-names) and [Result ids](#result-ids) below. ## The key ids | Id | What it is | Where to get it | | - | - | - | | `merchant_id` | Which account you're acting for. | Omit it to act as **yourself** (taken from your token). To act for a managed account, pick one with `jobmojito_configuration` / `list_my_merchants`. Only admins & sub-merchants may override it. | | `interview_template_id` | The template an interview is based on. **Its `type` decides the modality**: `interactive_elevenlabs` = voice-only, `interactive_heygen` = realtime avatar, `offline_heygen` = pre-recorded avatar (`offline_elai`/`offline_synthesia` are legacy and can't create new interviews). Required to create an interview. | `list_avatars` (`GET /merchant-avatar-list`) → each item's **`id`** (and `type`). | | interview definition id | The interview / position itself (its questions & config). | Returned as `interview_def_set_id` when you create one; listed as **`id`** by `list_interviews` (`GET /merchant-interview-list`). See the [three names](#the-interview-id-has-three-names) below. | | candidate id | A candidate's enrolment (a "profile interview"). | `list_candidates` (`GET /merchant-candidate-list`) → **`id`**. (`profile_id` is the underlying person, rarely needed.) | | `interview_result_id` | One completed interview attempt / result. | `list_interview_results` (`GET /merchant-result-list`) → **`interview_result_id`**. | | `knowledge_base_store_id` | A knowledge base store used to ground question generation. | See [Manage a knowledge base](/cookbooks/manage-knowledge-base). | | catalogue directory id | A page of the coaching catalogue, in every language it has. Not a uuid — a slug you choose (`sales-coaching`), which is also the URL segment `/catalogue/` and the value other directories reference in `tags_sub`. | You pick it when calling `create_catalogue_directory`; it is the `id` field of `update_catalogue_directory`. A directory renamed in the admin keeps answering to its old id in `get_catalogue_directory`, whose `id` shows the current one. | ## The interview id has three names When you create an interview you get back an `interview_def_set_id`. That **same value** is what every "which interview?" field wants — but the field is named differently per endpoint. Don't be thrown by the different names: | To do this | Tool | Field to put the interview id in | | - | - | - | | Read its definition (and its questions) | `get_interview_definition` (`GET /job-interview-get`) | `position_id` | | Change its settings or questions | `update_interview` (`POST /job-interview-update`) | `position_id` | | Change its state | `set_interview_state` (`POST /job-interview-set-state`) | `position_id` | | Register / invite candidates | `register_users_for_interview` (`POST /job-interview-register-users`) | `interview_id` | | Generate a public URL | `generate_interview_url` (`POST /job-interview-token`) | `interview_id` | | List its results | `list_interview_results` (`GET /merchant-result-list`) | `interview_id` (as a filter) | `position_id` and `interview_id` both accept either an interview-definition id or a position id — for the common case they're the same value you got back as `interview_def_set_id`. ## Result ids `list_interview_results` returns rows with several ids. Use the right one: * **`interview_result_id`** — the id you pass to `get_interview_result_details` (`GET /job-interview-details`, as the query param `interview_result_id`) and to `generate_interview_report` (`POST /job-interview-pdf`). **This is the one you almost always want.** * `id` — the *position result row* id. **Not** accepted by the detail/report endpoints; don't use it there. * `profile_interview_id` — the candidate id (same family as `list_candidates.id`). * `interview_result_pre_screening_id` — the pre-screening result, when present. ## Admin app links (build them yourself) There is no link-building tool. To let a user open something in the JobMojito admin app, construct the URL from the right id and present it as a clickable Markdown link. Base: `https://app.jobmojito.com`. | To open | URL | Id to use | | - | - | - | | A candidate | `https://app.jobmojito.com/candidates/{id}` | candidate id (`list_candidates.id`) | | An interview | `https://app.jobmojito.com/interview_creator/{id}` | interview definition id | | A result | `https://app.jobmojito.com/interview_results/result/{id}` | `interview_result_id` | URL-encode ids that contain spaces or other reserved characters. If you only have a **name**, resolve the id first (next section), then build the link. ## Finding an id when you only have a name You have two options: 1. **Ask the API.** The list tools accept a `filter_text` query parameter — e.g. `list_interviews(filter_text="Backend Engineer")`, `list_candidates(filter_text="peter@…")`, `list_interview_results(filter_text="…")` — then read the id from the matching row (mind [which id](#result-ids) for results). 2. **Use the admin UI.** In [app.jobmojito.com](https://app.jobmojito.com): interviews live under **Interview Creator**, candidates under **Candidates**, and completed interviews under **Interview Results**. Each list has a search box; open the item and its id is the last segment of the page URL (matching the link patterns above). # JobMojito MCP server Source: https://developer.jobmojito.com/mcp/overview Connect an AI agent to JobMojito over the Model Context Protocol (MCP): endpoint, Supabase OAuth sign-in, and merchant scoping. The **JobMojito MCP server** exposes the JobMojito hiring platform — interviews, candidates, invitations, pre-screening, knowledge bases, analytics — plus documentation search as **[MCP](https://modelcontextprotocol.io) tools**. Point any MCP-capable client (Claude, or your own agent built on an MCP SDK) at it and the model can create interviews, invite candidates, pull results, and search these docs on your behalf. Every tool runs **as the signed-in user**: the server forwards your Supabase access token to the JobMojito API on each call, so results respect your own permissions. Listing the available tools works without signing in, so a client can show you what the server does first — but **calling** any tool requires a signed-in token. See [Data & privacy](/mcp/data-and-privacy) for exactly what is and isn't exposed before sign-in. The MCP server is a convenience layer over the same [HTTP API](/how-the-api-works). For server-to-server integrations you usually want the API directly with a bearer token; use MCP when an **AI agent** should drive JobMojito interactively. ## Endpoint Connect your MCP client to: ```text theme={null} https://mcp.jobmojito.com/mcp ``` It speaks **Streamable HTTP** and is served directly (not behind a proxy auth gateway) so it can carry your Supabase token end to end. ## Authentication (Supabase OAuth) The server is an OAuth **resource server**; the authorization server is the JobMojito Supabase project. On first connection your client runs the standard OAuth flow: 1. The client discovers the auth server from `https://mcp.jobmojito.com/.well-known/oauth-protected-resource/mcp`. 2. You're sent to the JobMojito sign-in / consent screen (hosted by Supabase at `app.jobmojito.com`) to log in and approve access. 3. The client receives a token and attaches it to every MCP request. 4. The server forwards that token to the JobMojito API for each tool call. If a tool returns an authentication error (`401` / `invalid_token`), the session isn't signed in — re-authorize the connection and retry. See [Authentication](/authentication) for how tokens map to environments and permissions. ### Before you connect An agent with a signed-in connection can read candidate records, transcripts and scores, and that content is processed by whichever AI provider you connect. Read [Data & privacy](/mcp/data-and-privacy) before pointing this at a production account, and [Responsible use](/mcp/responsible-use) for the human-review and candidate-disclosure obligations that apply to AI in hiring. ## Connect your agent Pick your platform for step-by-step setup. Every path uses the same endpoint (`https://mcp.jobmojito.com/mcp`) and the OAuth sign-in above — no API key. ## Merchant scoping Most tools accept an optional `merchant_id`. Omit it to act on your **own** account; set it to act on behalf of a merchant you manage (agencies, resellers, sub-accounts). * MCP clients that can render UI get an interactive **`jobmojito_configuration`** picker — a searchable list of merchants. Pick one and it's applied to subsequent calls. * Clients that can't render UI use **`list_my_merchants`** (plain text, supports a `search` filter) and then pass `merchant_id` explicitly. ## Availability `GET https://mcp.jobmojito.com/healthz` returns a small JSON status document and never touches the upstream API — point an uptime monitor at it rather than at `/mcp`. ## What's next * [Tools reference](/mcp/tools) — the full tool inventory and the recommended docs-first calling pattern. * [Responsible use](/mcp/responsible-use) — human review, candidate disclosure, and what not to automate. * [Data & privacy](/mcp/data-and-privacy) — what an agent can reach, and where that data goes. * [Identifiers & admin links](/mcp/identifiers) — what each id means, which field wants it, and how to build admin app links. Read this if the model gets ids wrong. * [Skills](/skills/overview) — reusable patterns and best practices for building reliable agent workflows on top of these tools. # Responsible use Source: https://developer.jobmojito.com/mcp/responsible-use How JobMojito's AI output should be used in hiring: human review before decisions, disclosure to candidates, and what the platform will not do. JobMojito produces **assistive output for hiring workflows**. Interview scores, transcripts, summaries, pre-screening results and reports are decision-support material — inputs for a qualified human, not decisions in themselves. This page states the obligations that come with driving JobMojito from an AI agent. They are not optional extras: employment decisions are a designated high-risk category under the acceptable-use policies of every major AI provider whose clients can connect to this server, and under regulations including the EU AI Act and a growing number of US state and municipal hiring laws. ## The two obligations **1. A qualified human reviews before any decision is final.** No candidate is advanced, rejected or finally ranked *by the platform*. A person with relevant expertise must review AI-generated content before it informs or finalises an employment decision. Build your workflow so that a human sees the transcript and analysis and makes the call. **2. Candidates are told AI is used.** Anyone being assessed must be informed that AI is part of the process — at minimum at the start of each session. JobMojito surfaces this in the candidate experience, and your own careers pages, invitation emails and job postings should say so too. Where local law requires explicit consent, obtain it before the interview begins. ## What this means when an agent is driving An AI agent connected over MCP can create interviews, invite candidates and read results. That makes it easy to build a pipeline with no human in it. Don't. **Appropriate agent use** * Drafting and configuring interviews for a recruiter to review and publish. * Registering candidates a recruiter has already selected. * Retrieving, summarising and comparing results for a human to evaluate. * Generating reports that a hiring team reads and discusses. * Answering questions about the platform, credits, or a position's setup. **Not appropriate** * Automatically rejecting, advancing or ranking candidates without human review. * Presenting a JobMojito score as a hiring decision, or as an objective measure of a person's ability. * Inferring emotional state, personality traits, health, disability or protected characteristics from a candidate's voice, face or wording. * Using interview recordings to build biometric identification datasets. * Screening on criteria that are proxies for protected characteristics. If you write prompts or agent instructions on top of these tools, do not instruct the model to make the final call. Ask it to *prepare* a recommendation with its reasoning and the underlying evidence, and to state plainly that a human must decide. Agents follow the framing they are given. ## Language to reuse For candidate-facing material: > This interview is conducted with an AI interviewer. Your responses are recorded and analysed to produce a summary for the hiring team. A member of the hiring team reviews the results — no decision about your application is made automatically. You can request a human alternative by contacting us. For agent system prompts: > JobMojito scores, transcripts and reports are decision-support material, not hiring decisions. Never describe a candidate as accepted, rejected or finally ranked. Present findings with their evidence and note that a qualified human must review before anything is decided. The MCP server ships this framing in its own instructions, so a connected model receives it automatically — but that does not override how *you* prompt the model. ## Fairness and record-keeping * **Apply the same process to every candidate** for a given role: same questions, same rubric, same review. * **Keep the human decision auditable.** Record who reviewed what and when — several jurisdictions require you to be able to show this. * **Offer an alternative.** Candidates who cannot or prefer not to use an AI interview should have another route, and asking for it must not disadvantage them. * **Watch for drift.** Review outcomes periodically for disparate impact across demographic groups. JobMojito gives you the transcripts and scores to do this; the analysis is your responsibility as the employer. ## Who is responsible for what JobMojito is a **processor**: we run the interview and produce the analysis on your instructions. You — the employer or agency operating the account — are the **controller**, and you decide how the output is used. The lawful basis for processing, the candidate notice, the human review step and the outcome are yours. See [Data & privacy](/mcp/data-and-privacy) for the data-flow detail. ## Related * [Data & privacy](/mcp/data-and-privacy) — what the MCP tools touch and where it goes. * [Privacy Policy](https://jobmojito.com/privacy_policy) — human oversight, automated decision-making and biometric processing. * [Trust centre](https://trust.jobmojito.com) — sub-processors and security posture. # MCP tools reference Source: https://developer.jobmojito.com/mcp/tools The JobMojito MCP tool inventory by category, how to build admin UI deep-links, and the docs-first calling pattern agents should follow. The MCP server exposes three kinds of tools: **documentation search**, the **JobMojito API** (auto-generated from the [OpenAPI spec](/how-the-api-works), one tool per endpoint with a curated name), and **merchant selection**. Tool descriptions are prefixed with a `[Category]` label so agents can group them. ## Docs-first calling pattern The server's instructions ask agents to **understand before acting**: 1. To learn how an endpoint, field or workflow behaves, `search_documentation` is faster and more reliable than experimenting; `get_documentation(url)` then reads the page in full. Calling action tools speculatively just to discover their inputs is the slow path. 2. `search_documentation` is a single entry point: one call searches both this developer/API reference and the [help center](https://help.jobmojito.com) in parallel. 3. For multi-step work, search for a step-by-step cookbook and follow it rather than chaining tools by trial and error. 4. Prefer the read-only **list** tools to look things up before creating or changing anything. ## Read-only vs. state-changing Every tool carries standard MCP annotations, so a client knows whether it can run without asking you first: * **`readOnlyHint: true`** — only reads. All `list_*` and `get_*` tools, plus the documentation tools and the merchant picker. Most clients run these without a confirmation prompt. * **`destructiveHint: true`** — creates, changes or sends something. Everything else, including report generation and link creation, which consume credits or produce shareable artifacts. Clients should ask before running these, and you should let them. Any newly added endpoint is treated as state-changing until it is explicitly curated, so the safe default is never the silent one. ## Documentation | Tool | Purpose | | - | - | | `search_documentation` | Search developer docs + help center in parallel. Read-only. | | `get_documentation` | Fetch a documentation page by URL. Read-only. | ## Configuration / merchants | Tool | Purpose | | - | - | | `jobmojito_configuration` | Interactive, searchable merchant picker (UI clients). Run it whenever a `merchant_id` is needed but none is selected. | | `list_my_merchants` | Text list of your merchants (supports a `search` filter) — the fallback for non-UI clients. | Pass the chosen `merchant_id` on subsequent calls; omit it to act on your own account. See [Merchant scoping](/mcp/overview#merchant-scoping). ## Interview — create & manage | Tool | Endpoint | Purpose | | - | - | - | | `create_interview` | `POST /job-interview-create` | Create an interview with AI-generated questions from a job description. | | `create_interview_from_questions` | `POST /job-interview-create-from-array` | Create an interview from your own array of questions. | | `create_persona` | `POST /persona-create` | Create a role-play (avatar plays a defined role; no AI question generation). **Set `portal` deliberately** — `interview` for assessing/screening candidates (recruiter-visible, invited, merchant-billed); `coaching` is the default and creates a coaching-portal practice persona instead. Also set `welcome_message`, `thank_you_message` and `candidate_expectations`, which otherwise fall back to generic defaults, and `persona_avatar_progress` — without it the avatar has no arc and tends to either concede immediately or never concede at all. | | `get_interview_definition` | `POST /job-interview-get` | Fetch an interview's definition/configuration, **including its ordered `questions` array** — in the same format `create_interview_from_questions` accepts, so it can be edited and sent straight back to `update_interview`. | | `update_interview` | `POST /job-interview-update` | Change the settings of an existing interview or position — name, description, avatar template, recording, scoring, tags and the rest. Only the fields you send are written. To change the questions, send `questions`: the **whole** list you want, applied as a diff (see below). **Omit `questions` and the existing questions are untouched.** Still out of scope: the welcome / thank-you messages and the instructional-video screen (stored as steps, not questions), and the language (the questions are already written in it). | | `set_interview_state` | `POST /job-interview-set-state` | Change lifecycle state / manage the iframe embed key. | | `generate_interview_url` | `POST /job-interview-token` | Generate a signed public interview URL. | | `request_another_interview_attempt` | `POST /job-interview-result-request-another-attempt` | Re-open a submitted result so the candidate can retry. Grants an attempt even past the `interview_attempts` cap. | | `register_users_for_interview` | `POST /job-interview-register-users` | Register candidates for an interview and get their links (batch, per-row results). | Read before you write: `questions` is **replaced wholesale**, so call `get_interview_definition` first, edit the array it returns, and send the whole list back — there is no per-question call. Keep each question's `id` on the ones you did not mean to change; that is what marks a question as unchanged. It is applied as a **diff**, matched on `external_id`, then `id`, then identical content. Unchanged questions keep their existing record (and with it their answer rules and any rendered avatar video); an edited one is unlinked and re-created; one you dropped is unlinked. Nothing is ever deleted — questions are shared records, so removal only unlinks them from this interview. `questions_diff` in the response reports exactly what was decided. **Re-sending an unchanged array is a no-op** — no write happens at all — so the read → edit → write loop is safe to repeat, and safe to run when you are not sure anything changed. Questions of an `active` interview can only be changed on the interactive avatar templates (where re-publishing is instant); on `offline_*` set `status` to `draft` first. Full walkthrough: [Edit interview questions](/cookbooks/edit-interview-questions). ## Coaching catalogue Coaching-platform only. The catalogue is a tree of **directories**; each directory is one page at `/catalogue/` on the merchant's coaching portal. | Tool | Endpoint | Purpose | | - | - | - | | `list_catalogue_directories` | `GET /catalogue-tag-list` | Find directories: search by text, filter by language/status/visibility, or pass `parent_tag` to list one directory's children in display order. `is_start_directory` marks the catalogue's root. | | `get_catalogue_directory` | `GET /catalogue-tag-get` | One directory in full — its `content_md`, its resolved sub-directories, and **the sessions its tag filter currently matches**, which is how you check a session really landed in it. Its other languages are in `translations`; pass `mojito_language_code` to see the sub-directories and sessions of that language's version. | | `create_catalogue_directory` | `POST /catalogue-tag-create` | Create a directory. Pass `parent_tag` to nest it under an existing one. | | `update_catalogue_directory` | `POST /catalogue-tag-update` | Rename a directory, re-point which sessions it lists, re-order its sub-directories, author its custom page, or add, replace and remove a language version (`translations`). | Read before you write: `content_md`, `tags_sub` and `tags_interview_set_filter` are **replaced wholesale**, so fetch the current value with `get_catalogue_directory` and send the extended list — not just your additions. `translations` is the exception: it is a patch per language (an object replaces that language's version, `null` removes it, languages you do not send are kept). **Several languages.** One directory can hold every language of a page. Its top-level fields are the default language (`mojito_language_code`); each entry of `translations` (`{"sk": {"name": …, "tags_interview_set_filter": ["sales-sk"], …}}`) is another language's version with its own text, sub-directories and session filter. Sessions have one language each, so create one session per language with a language-specific tag and give each version that tag as its filter. Learners switch languages on the page. Walkthrough: [Several languages in one directory](/cookbooks/build-a-coaching-catalogue#several-languages-in-one-directory). Two fields do the real work: * **`tags_interview_set_filter`** selects the sessions. A coaching or persona session appears in the directory when the session's own `tags` contain **every** tag in this list (an AND, not an OR), it is `active`, and its visibility is `public` or `merchant_public`. Set the matching `tags` on the session with `create_interview`, `create_interview_from_questions`, `create_persona` or `update_interview`. * **`content_md`** turns the directory into a custom page. With it set, your Markdown replaces the default grid and decides the layout; leave it null for the plain grid. These directives each go **alone on their own line**: | Directive | Renders | | - | - | | `[plan-progress]` | The learner's coaching-plan progress. | | `[directory:]` | A card for one sub-directory. | | `[session:]` | A card for one session. | | `[sessions]` | Every session in this directory. | | `[sessions:]` | Sessions matching a term. | | `[sessions:filter=,limit=]` | A filtered, capped list. | ## Results & reports | Tool | Endpoint | Purpose | | - | - | - | | `get_interview_result_details` | `POST /job-interview-details` | Full result with transcript and AI analysis. | | `generate_interview_report` | `POST /job-interview-pdf` | Generate an HTML / PDF / JSON report for a completed interview. | ## Knowledge base | Tool | Endpoint | Purpose | | - | - | - | | `upload_knowledge_base_document` | `POST /knowledge-base-document-upload` | Upload a document the AI can draw questions/context from. | ## Merchant lists (read-only) | Tool | Purpose | | - | - | | `list_interviews` | List the merchant's interview definitions. | | `list_candidates` | List candidates. | | `list_interview_results` | List interview results. | | `list_avatars` | List available interviewer avatars. | | `list_sub_merchants` | List sub-merchants you manage. | | `get_merchant_analytics` | Fetch merchant daily event analytics. | | `get_merchant_status` | Status snapshot: credit balances, subscription, pending-work counts, candidate/result totals, and invitation headroom. | | `get_merchant_credit_usage` | Fetch the merchant's credit usage. | | `list_languages` | List supported platform (mojito) languages: code, English/local names, SVG flag URL, per-interface enablement (coaching/interview/admin), and Azure speech accents. | New JobMojito endpoints appear as tools automatically (with an auto-generated name) even before they're curated here, so this table can lag slightly behind the live tool list. The [API Reference](/how-the-api-works) is the source of truth for request/response schemas. ### Not exposed as MCP tools A few endpoints are intentionally **excluded** from the MCP surface (they're administrative or one-shot flows not suited to general agent use) but remain fully available over the [HTTP API](/how-the-api-works): bulk `invite-users`, `create-for-candidate-with-token`, and the pre-screening endpoints (`pre-screening-create`, resume text/binary pre-screen). Call these directly with a bearer token when you need them. ## Result size Tool results are capped so a client never receives a silently truncated response. If a call exceeds the limit it returns an error asking you to narrow the request rather than a half-complete answer — page with `limit` and `offset`, or filter by `merchant_id`, a date range or a status. ## Identifiers & admin links Which id goes in which field is the most common source of errors — and the same interview id is named `interview_def_set_id`, `position_id`, or `interview_id` depending on the endpoint. See [**Identifiers & admin links**](/mcp/identifiers) for the full id glossary, the "same id, different field name" tables, the admin app link patterns, and how to resolve an id from a name. The MCP server's own instructions point agents at that guide before they pass an unfamiliar id. # Skills Source: https://developer.jobmojito.com/skills/overview Reusable patterns (skills) for driving JobMojito from AI assistants — the docs-first loop, merchant selection, safe writes, and reliable multi-step workflows. JobMojito is built to be driven by **AI assistants**, not just humans. This section covers the reusable patterns — call them *skills* — that make automated hiring workflows reliable, whether you connect through the [MCP server](/mcp/overview) or call the [HTTP API](/how-the-api-works) from your own agent framework. It's tool-agnostic and complements the concrete [MCP tools reference](/mcp/tools). ## The core loop: understand → look up → act Agents get into trouble when they call write actions speculatively. Structure the loop instead: 1. **Understand.** Before creating or changing anything, search the documentation (`search_documentation` over MCP, or these docs directly) to confirm the right endpoint and what each input means. Don't probe write tools to discover their schema. 2. **Look up.** Use the read-only **list** tools/endpoints (`list_interviews`, `list_candidates`, `list_interview_results`, …) to resolve names to ids and check current state before acting. Mind which id goes in which field — the same interview is called `interview_def_set_id`, `position_id`, or `interview_id` on different endpoints, and results need `interview_result_id`, not the row's `id`. See [Identifiers & admin links](/mcp/identifiers). 3. **Act.** Call the write action once you know the exact tool and inputs. 4. **Verify.** Read the result back (e.g. `get_interview_result_details`) rather than assuming success — especially for batch endpoints (see below). ## Pick the merchant first Most actions are **merchant-scoped**. If the user manages more than one account, resolve the merchant *before* the first write: * Over MCP, render the interactive `jobmojito_configuration` picker (or use `list_my_merchants` for non-UI clients), then pass the chosen `merchant_id` on every subsequent call. Omit it to act on the user's own account. * Never guess a merchant. If it's ambiguous, ask the user. ## Follow cookbooks for multi-step work Hiring flows are multi-step. Rather than chaining tools by trial and error, follow the published recipes: * [Create an interview](/cookbooks/create-an-interview) * [Invite candidates](/cookbooks/invite-candidates) * [Pre-screen candidates](/cookbooks/pre-screen-candidates) * [Review results](/cookbooks/review-results) * [Manage a knowledge base](/cookbooks/manage-knowledge-base) * [Build a coaching catalogue](/cookbooks/build-a-coaching-catalogue) A typical flow: *create the interview → generate URLs / register candidates → review results → generate a report.* ## Handle batch endpoints per-row `invite-users` and `register_users_for_interview` process each row independently and **always return HTTP 200** with a results array. A bad row doesn't fail the whole call — it carries its own error while the others succeed. Agents must inspect each row's `result` field, not just the HTTP status, and surface the failures back to the user. See [Batch endpoints](/how-the-api-works#batch-endpoints-return-per-row-results). ## Give the user clickable links When a workflow produces something a person will want to open, present a Markdown link to the JobMojito admin rather than raw ids. Build the URL from the entity id using the documented [admin link patterns](/mcp/identifiers#admin-app-links-build-them-yourself), e.g. `https://app.jobmojito.com/interview_results/result/{id}`. ## Respect permissions and cost * **Permissions.** Every call runs as the signed-in user (the token is forwarded), so the agent can only do what that user can. Treat `401`/`403` as "re-authorize or not permitted", not something to work around. * **Credits.** Some actions consume account credits (creating interviews, running pre-screens, generating reports). For bulk or automated runs, make the cost visible to the user and confirm before large batches. See [How credits work](/how-credits-work). ## Safe-writes checklist Before an agent performs a write, it should be able to answer: * Which **merchant** am I acting as? * Have I **read the docs / looked up ids** for this action? * Is this **idempotent**, or will a retry double-charge or double-invite? * For **batches**, will I inspect every row's `result`? * Will I **report** links and per-row outcomes back to the user? ## Where to go next * [MCP server](/mcp/overview) — connect an agent over MCP. * [MCP tools reference](/mcp/tools) — the full tool inventory. * [How the API works](/how-the-api-works) — base URL, errors, and the HTTP endpoint list for custom agents. # Webhooks Source: https://developer.jobmojito.com/webhooks Receive real-time interview events at your own endpoint, with at-least-once guaranteed delivery and automatic retries. Webhooks let JobMojito notify **your** server the moment something happens - rather than you polling the API. When an interview or assessment reaches a milestone, we send an HTTP `POST` with a JSON payload to a URL you configure. The full payload schema is published as its own OpenAPI document: ```text theme={null} https://cool.jobmojito.com/functions/v1/openapi-webhooks ``` ## Configuring a webhook A webhook is configured per merchant and per environment, with: * **URL** - your HTTPS endpoint that will receive the `POST`. * **Events** - which events this endpoint is subscribed to (see below). * **Headers** - optional custom headers we send with every delivery (use these to authenticate the request - see [Securing your endpoint](#securing-your-endpoint)). Configure webhooks from the JobMojito admin, or ask your account contact. Because webhooks are environment-scoped, a staging endpoint never receives production events. An event is delivered to the webhooks whose environment matches the **interview's** `environment`. See [Environments and testing](/how-the-api-works#environments-and-testing). ## Events Subscribe to one or more of these events: | Event | Sent when | | - | - | | `interview_result` | An interview or assessment is **submitted by the candidate** (the result is finalized and AI-scored). If the interview is configured to generate a PDF report, this is **delayed until the report is ready** so `pdf_export_url` is included. Also re-sent when a result field changes afterwards (score, recruiter decision, AI analysis, PDF URL). | | `interview_collected` | The candidate finishes and their raw answers are collected, **before** final AI scoring. | | `interview_failed` | An interview does not complete successfully - failed, abandoned, or terminated with no speech detected. Scoring/AI fields are typically empty for this event. | ### Exactly when each event fires These are the precise conditions the platform evaluates on an interview result (verified against the database triggers). All three only apply to results of type `interview` or `assessment`. * **`interview_result`** - fires when the result becomes `status = active` with `processing_status = completed` and the interview status is `completed` or `completed, stopped early` (i.e. the candidate submitted it), **and** one of these fields changed: status, processing status, interview status, score, recruiter AI score, recruiter AI analysis, why-hire / why-not-hire, the recruiter next-round decision, or the PDF report URL. If PDF report generation is enabled, the event is held until `pdf_export_url` is populated, then sent. * **`interview_collected`** - fires while the result is still `status = draft` and the interview status **changes to** `completed` or `completed, stopped early` (answers collected, not yet finalized/submitted). * **`interview_failed`** - fires while the result is still `status = draft` and the interview status **changes to** `failed`, `abandoned`, or `terminated, no speech detected`. Because `interview_result` re-fires whenever one of the watched fields changes (e.g. a recruiter later marks "go to next round"), you may receive it more than once for the same `id` - see [idempotency](#design-your-handler-to-be-idempotent). ## The payload Every event delivers the same JSON shape. Key fields: | Field | Description | | - | - | | `id` | The interview result id this event is about. | | `interview_id` | The interview definition id. | | `status` / `interview_status` / `processing_status` | Lifecycle status of the result. | | `profile_interview_id` / `profile_id` | Candidate identifiers. | | `candidate_name` / `candidate_email` / `candidate_phone_number` / `candidate_external_id` | Candidate details (when a candidate is linked). | | `score` / `ai_analysis_recruiter_score` | Scores. | | `ai_analysis` / `ai_analysis_recruiter` | AI analysis (candidate-facing / recruiter-facing text). | | `ai_analysis_other` | Session-level AI analyses keyed by name: `strengths_concerns` (`strengths` / `concerns`, up to 4 each - named `hiring_reasons` with `reasons_to_hire` / `reasons_not_to_hire` before 2026-09-15), `takeaways` (coaching sessions), `multi_department`. | | `ai_analysis_recruiter_why_hire` / `ai_analysis_recruiter_why_not_hire` | Deprecated copies of `ai_analysis_other.strengths_concerns`, kept for existing integrations. | | `recruiter_go_next_round` | Recruiter decision to advance the candidate. | | `pdf_export_url` | PDF report URL - only when PDF report generation is enabled for the interview. | | `reason` | Which field(s) changed to trigger this delivery. | ```json theme={null} { "id": "…", "interview_id": "…", "status": "active", "interview_status": "completed", "candidate_name": "Jane Doe", "candidate_email": "jane@example.com", "score": 7.2, "ai_analysis_recruiter": "…", "recruiter_go_next_round": true, "pdf_export_url": "https://…/report.pdf", "reason": "score" } ``` See the [webhooks OpenAPI spec](https://cool.jobmojito.com/functions/v1/openapi-webhooks) for the complete field list. ## Guaranteed delivery Webhook delivery is **at-least-once** and resilient - we don't drop events if your endpoint is briefly down. * **Queued & retried.** Every delivery is queued and recorded. If your endpoint doesn't respond with a `2xx`, or doesn't respond within **60 seconds**, the delivery is **retried automatically**. * **Up to 10 attempts.** We retry up to **10 times** before marking the delivery `gave_up`. Every attempt - request and response - is stored in your delivery history so you can see exactly what happened. * **Acknowledge fast.** Return any `2xx` status as soon as you've safely accepted the event (e.g. written it to a queue/DB). Do slow work afterwards - if your handler is slow, the delivery may time out and be retried. ### Design your handler to be idempotent Because delivery is at-least-once, **you may receive the same event more than once** (a retry, or a re-send when a field changes). Each event is a snapshot of the current state of the result identified by `id`, so the safe pattern is to **upsert by `id`** and apply the latest payload. Applying the same event twice must be harmless. Do **not** assume strict ordering between deliveries - use the payload's `status` fields and `reason` to understand what changed rather than relying on arrival order. ## Securing your endpoint Deliveries aren't signed with an HMAC. Instead, JobMojito sends the custom **headers** you configured on the webhook with every request (and retry). Use this to authenticate the call - for example, set a secret header or `Authorization` value that only you know, and reject any request that doesn't carry it: ```http theme={null} POST /your/webhook HTTP/1.1 Authorization: Bearer Content-Type: application/json ``` Additional good practice: * Serve the endpoint over **HTTPS** only. * Verify the secret header before processing the body. * Be tolerant of new fields - we may add fields to the payload over time. ## Replaying / inspecting deliveries Each delivery attempt is recorded with its request and response. If an endpoint was down, you can review the history and re-trigger a delivery from the admin once it's back online. # Welcome to the JobMojito API Source: https://developer.jobmojito.com/welcome Build AI-powered interviews, assessments and candidate pre-screening into your own product with the JobMojito API and webhooks. The **JobMojito API** lets you bring AI-driven interviewing, assessments and candidate pre-screening into your own products and workflows. Anything you can do in the JobMojito admin - create an interview, invite candidates, run a resume pre-screen, pull results and transcripts, generate a PDF report - you can do programmatically. This is the developer documentation. For product and platform concepts (interview workflow, data privacy, multilingual support, custom domains, and more), see the [Platform Deepdive](https://help.jobmojito.com/collections/4520925-platform-deepdive) in our help center. ## What you can build * **Create interviews & assessments** - generate an AI interview from a job description, or supply your own questions. * **Pre-screen candidates** - score resumes against a position before anyone spends time interviewing. * **Invite & register candidates** - onboard candidates and hand them a ready-to-use interview link. * **Collect results** - fetch scores, AI analysis and full transcripts, or a branded PDF report. * **React in real time** - receive [webhooks](/webhooks) when a candidate submits an interview, so your ATS or workflow stays in sync. ## How the documentation is organized | Section | What's inside | | - | - | | [Authentication](/authentication) | How to authenticate your API requests. | | [How the API works](/how-the-api-works) | Base URL, environments, request/response format, errors, and the full list of endpoints. | | [Webhooks](/webhooks) | Subscribing to events and our delivery guarantees. | | API Reference | Per-endpoint request/response details, generated from our OpenAPI spec. | ## Getting started 1. Get your API credentials from the JobMojito admin (or ask your account contact). 2. Read [Authentication](/authentication) and make your first call to `POST /job-interview-get`. 3. Subscribe to [webhooks](/webhooks) so you're notified when results are ready. ## Need help? Reach out to your JobMojito contact or email [**support@jobmojito.com**](mailto:support@jobmojito.com) - we're happy to help with integration.