> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jobmojito.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

## 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_recruiter_why_hire` / `ai_analysis_recruiter_why_not_hire` | AI analysis.                                                                   |
| `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

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 <the-secret-you-configured>
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.
