{"openapi":"3.1.0","info":{"title":"JobMojito Webhooks","version":"1.0.0","description":"Outgoing webhooks JobMojito POSTs to your configured endpoint(s) when an interview or assessment reaches a lifecycle milestone, and (webhooks v2) when an application is submitted, changes step, or the candidate is hired or updated. Configure the target URL, subscribed events and custom auth headers per environment in your webhook settings. Respond with any 2xx to acknowledge; failed deliveries are retried.\n\n## Verifying signatures\n\nGive a webhook a signing secret (Generate secret in the webhook settings; it is shown once). Every request of that webhook then carries `X-JobMojito-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>` and `X-JobMojito-Delivery: <delivery id>` (the same on every retry of one delivery, so you can drop duplicates); v2 events also send `X-JobMojito-Event`. To verify: take `t` and `v1` from the header, compute the HMAC-SHA256 of `t + \".\" + raw request body` (the bytes as received, not re-serialised JSON) with your secret, compare it with `v1` in constant time, and reject the request when `t` is more than 300 seconds away from your clock. Retries are signed again with a fresh timestamp. After you rotate the secret, requests are signed with the new one only."},"webhooks":{"interview_result":{"post":{"tags":["Webhooks"],"summary":"Interview result submitted","description":"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).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookInterviewResultPayload"}}}},"responses":{"200":{"description":"Return any 2xx status to acknowledge receipt. Non-2xx or a timeout causes the delivery to be retried."}}}},"interview_collected":{"post":{"tags":["Webhooks"],"summary":"Interview answers collected","description":"Sent when the candidate finishes the interview and their raw answers are collected, before final AI scoring.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookInterviewResultPayload"}}}},"responses":{"200":{"description":"Return any 2xx status to acknowledge receipt. Non-2xx or a timeout causes the delivery to be retried."}}}},"interview_failed":{"post":{"tags":["Webhooks"],"summary":"Interview failed","description":"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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookInterviewResultPayload"}}}},"responses":{"200":{"description":"Return any 2xx status to acknowledge receipt. Non-2xx or a timeout causes the delivery to be retried."}}}},"application.submitted":{"post":{"tags":["Webhooks"],"summary":"Application submitted","description":"Sent once per application: when the candidate submits the application form (pre-screening), or, for a position without one, when the application is created.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookV2Envelope"}}}},"responses":{"200":{"description":"Return any 2xx status to acknowledge receipt. Non-2xx or a timeout causes the delivery to be retried."}}}},"application.step_changed":{"post":{"tags":["Webhooks"],"summary":"Application step changed","description":"Sent when the current step of an application or the decision on it changes: the candidate moved on to the next step (`previous_step` is set), or the step was accepted, rejected, sent for review or decided by the AI.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookV2Envelope"}}}},"responses":{"200":{"description":"Return any 2xx status to acknowledge receipt. Non-2xx or a timeout causes the delivery to be retried."}}}},"candidate.hired":{"post":{"tags":["Webhooks"],"summary":"Candidate hired","description":"Sent when the outcome of an application (or of a stand-alone interview) becomes Hired, with the start date when the recruiter set one, the source with its corrections and the candidate's documents. Use it to create the new hire in your HR system.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookV2Envelope"}}}},"responses":{"200":{"description":"Return any 2xx status to acknowledge receipt. Non-2xx or a timeout causes the delivery to be retried."}}}},"candidate.updated":{"post":{"tags":["Webhooks"],"summary":"Candidate updated","description":"Sent when the candidate's name, e-mail, phone number, LinkedIn, address or external id changes. Changes within 5 minutes are sent as one delivery carrying the latest data.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookV2Envelope"}}}},"responses":{"200":{"description":"Return any 2xx status to acknowledge receipt. Non-2xx or a timeout causes the delivery to be retried."}}}}},"components":{"schemas":{"WebhookStep":{"type":"object","properties":{"id":{"type":"string","description":"position_result_step id."},"type":{"type":"string","description":"Step type: pre-screening, interview, meeting, ..."},"name":{"type":"string","description":"Step name as the recruiter sees it."},"sequence_no":{"type":"number","description":"Position of the step in the process (1 = first)."},"status":{"type":"string","description":"Step status: active, completed, archived, ..."},"decision_status":{"type":"string","description":"Decision on the step: candidate_action, processing, recruiter_action, ai_accept, ai_reject, recruiter_accept, recruiter_reject."}},"required":["id"],"description":"One step of an application."},"WebhookApplication":{"type":"object","properties":{"id":{"type":"string","description":"Application id (position_result id); for a stand-alone interview the interview result id."},"status":{"type":"string","description":"Application status."},"decision_status":{"type":"string","description":"Decision on the current step (applications only)."},"outcome":{"type":"string","enum":["pending","hired","declined"],"description":"Hiring outcome."},"hire_start_date":{"type":"string","description":"Start date (YYYY-MM-DD) the recruiter set with the Hired outcome."},"created_at":{"type":"string","description":"When the application was created (ISO 8601)."},"is_test":{"type":"boolean","description":"true for a test application a recruiter ran."},"source":{"type":"object","additionalProperties":{"nullable":true},"description":"How the application came: channel, utm_*, referrer, landed_at, and `corrections` (array of {at, by, from, to, field, note}: changes a recruiter made to the source)."},"current_step":{"$ref":"#/components/schemas/WebhookStep"},"previous_step":{"allOf":[{"$ref":"#/components/schemas/WebhookStep"},{"description":"application.step_changed only: the step the candidate came from when the current step changed."}]}},"required":["id"],"description":"The application the event is about."},"WebhookPosition":{"type":"object","properties":{"id":{"type":"string","description":"Position id."},"name":{"type":"string"},"code":{"type":"string","description":"Your position code."},"department":{"type":"string"},"location":{"type":"string"},"category":{"type":"string","description":"Job category."},"tags":{"type":"array","items":{"type":"string"},"description":"Position tags (for example brands)."},"country_availability":{"type":"object","additionalProperties":{"nullable":true},"description":"Countries the position is open to."},"external_ref":{"type":"string","description":"Reference of the position in your HR system, when set."},"custom_fields":{"type":"object","additionalProperties":{"nullable":true},"description":"Custom fields of the position, when used."},"recruiter":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["id"],"description":"The recruiter responsible for the position."}},"required":["id"],"description":"The position of the application (absent for a stand-alone interview)."},"WebhookCandidate":{"type":"object","properties":{"id":{"type":"string","description":"Candidate id (profile_interview id)."},"external_id":{"type":"string","description":"Your id for the candidate."},"name":{"type":"string"},"email":{"type":"string"},"phone_number":{"type":"string"},"linked_in":{"type":"string"},"physical_address":{"type":"string"},"language":{"type":"string","description":"Candidate language code."},"first_source":{"type":"object","additionalProperties":{"nullable":true},"description":"How the candidate first came to you (first touch)."},"internal_employee":{"type":"object","additionalProperties":{"nullable":true},"description":"Employee record when the candidate is a current employee (when your HR system is connected)."}},"required":["id"],"description":"The candidate."},"WebhookDocument":{"type":"object","properties":{"id":{"type":"string","description":"Document id."},"type":{"type":"string","description":"resume, cover_letter, national_id or custom."},"name":{"type":"string","description":"File name or the name the document was given."},"created_at":{"type":"string"},"download_url":{"type":"string","description":"GET it with your API key (Authorization: Bearer); answers 302 to a download link valid for 10 minutes."}},"required":["id"],"description":"A document of the candidate."},"WebhookV2Envelope":{"type":"object","properties":{"event":{"type":"string","enum":["application.submitted","application.step_changed","candidate.hired","candidate.updated"],"description":"The event (also sent as the X-JobMojito-Event header)."},"event_id":{"type":"string","description":"Unique id of this event."},"occurred_at":{"type":"string","description":"When it happened (ISO 8601)."},"merchant_id":{"type":"string","description":"Your company id."},"environment":{"type":"string","enum":["production","uat","development","demo"],"description":"Environment of the position (of its first interview), the webhook environment it was routed to."},"data":{"type":"object","properties":{"application":{"$ref":"#/components/schemas/WebhookApplication"},"position":{"$ref":"#/components/schemas/WebhookPosition"},"sub_company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id"],"description":"The sub-company the application belongs to, when it is one."},"candidate":{"$ref":"#/components/schemas/WebhookCandidate"},"documents":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDocument"}},"interview":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id"],"description":"Stand-alone interview results only: the interview."}},"description":"What the event is about."}},"required":["event","event_id","occurred_at","merchant_id","environment","data"],"description":"Payload of the application and candidate events (webhooks v2). Empty fields are left out."},"WebhookInterviewResultPayload":{"type":"object","properties":{"id":{"type":"string","description":"interview_result id this event is about."},"interview_id":{"type":"string","description":"Interview definition id (question_set_id)."},"question_set_id":{"type":"string","description":"Same as interview_id (the interview_def_set id)."},"status":{"type":"string","description":"Result lifecycle status (e.g. active, draft)."},"interview_status":{"type":"string","nullable":true,"description":"Coach/interview status (coach_status), e.g. completed, \"completed, stopped early\", failed, abandoned."},"processing_status":{"type":"string","nullable":true,"description":"AI processing status (e.g. completed)."},"profile_interview_id":{"type":"string","nullable":true,"description":"Candidate profile_interview id."},"profile_id":{"type":"string","nullable":true,"description":"Candidate profile id (when a registered profile is linked)."},"candidate_external_id":{"type":"string","nullable":true,"description":"Your external id for the candidate (present when a candidate is linked)."},"candidate_name":{"type":"string","nullable":true,"description":"Candidate name (present when a candidate is linked)."},"candidate_email":{"type":"string","nullable":true,"description":"Candidate email (present when a candidate is linked)."},"candidate_phone_number":{"type":"string","nullable":true,"description":"Candidate phone number (present when a candidate is linked)."},"score":{"type":"number","nullable":true,"description":"Overall interview score."},"ai_analysis_recruiter_score":{"type":"number","nullable":true,"description":"Recruiter-facing AI score (score_interview_ai)."},"ai_analysis":{"type":"string","nullable":true,"description":"Candidate-facing AI analysis."},"ai_analysis_recruiter":{"type":"string","nullable":true,"description":"Recruiter-facing AI analysis."},"ai_analysis_other":{"nullable":true,"description":"Session-level AI analyses keyed by analysis name (jsonb). `strengths_concerns`: { strengths: string[], concerns: string[] } (0-4 items each, recruiter-facing; called `hiring_reasons` with `reasons_to_hire` / `reasons_not_to_hire` inside before 2026-09-15, and rows written before then may still carry that shape). `takeaways`: { what_went_well: string[], next_steps: { text, priority: impact | quick | longterm, guidance: { kind: say | structure | length | exercise | perspective, text }, proof? }[], fixed?: string[] } (coaching / persona sessions, learner-facing; `proof` entries are { kind: quote | ai_summary | transcript_ref | document, text, ref?: { result_question_id } }; `fixed` lists the steps from the previous attempt that the learner did this time). `multi_department`: the multi-department-selection analysis. `score_rationale`: the rubric bracket of the session score and evidenced counts. `key_statement`: { quote, result_question_id }, the one verbatim quotation a recruiter would remember the candidate by (interview / persona_interview, absent when nothing distinctive was said). `recruiter_next_step`: { suggested_next_step: advance | unsure | do_not_advance, next_step_reason, score_narrative_mismatch, score_interview_ai } (interview / persona_interview), the AI's suggested next step for the human reviewer, written once the session score is known; decision support only, never a hiring decision; `score_narrative_mismatch` is true when the suggestion contradicts the score band (score_interview_ai >= 7 with do_not_advance, < 5 with advance). Further keys may be added."},"ai_analysis_recruiter_why_hire":{"nullable":true,"description":"Reasons to hire (list). Deprecated: a copy of ai_analysis_other.strengths_concerns.strengths, kept for existing integrations.","deprecated":true},"ai_analysis_recruiter_why_not_hire":{"nullable":true,"description":"Reasons not to hire (list). Deprecated: a copy of ai_analysis_other.strengths_concerns.concerns, kept for existing integrations.","deprecated":true},"recruiter_go_next_round":{"type":"boolean","nullable":true,"description":"Recruiter decision to advance the candidate to the next round."},"pdf_export_url":{"type":"string","nullable":true,"description":"URL of the generated PDF report. Provided only when the interview is configured to generate a PDF report; null otherwise."},"reason":{"type":"string","nullable":true,"description":"Which watched field(s) changed to trigger this delivery (change reason)."}},"required":["id","interview_id","question_set_id","status","interview_status","processing_status","profile_interview_id","profile_id","score","ai_analysis_recruiter_score","ai_analysis","ai_analysis_recruiter","recruiter_go_next_round","pdf_export_url","reason"],"description":"Event payload POSTed to your configured webhook URL for interview/assessment lifecycle events."}}}}