{
  "openapi": "3.1.0",
  "info": {
    "title": "Whizz Hire API",
    "version": "1.0.0",
    "summary": "The hiring engine that reads every CV.",
    "description": "REST API for AI job posts, CV screening with quoted evidence, and CV-grounded interview kits. Long-lived work is asynchronous: creation endpoints return 202 with a run_id; poll GET /v1/runs/{id} or register a webhook for job.succeeded / job.failed, then fetch the finished resource. \"Jobs\" in this API are job posts (vacancies); background work is a \"run\". Every non-2xx response uses the envelope {\"error\": {\"code\", \"message\"}}. All authenticated responses carry x-ratelimit-limit / x-ratelimit-remaining / x-ratelimit-reset headers (per-key fixed 60-second window, default 60 requests/min). Compliance by construction: CVs are anonymized before scoring, evidence quotes are validated verbatim against the CV text, and the AI never rejects a candidate — screening recommends a top-K, humans disposition.",
    "contact": {
      "name": "Whizz Tech support",
      "email": "support@whizztech.ai",
      "url": "https://hire.whizztech.ai/docs"
    }
  },
  "servers": [
    {
      "url": "https://hire.whizztech.ai",
      "description": "Production"
    }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Jobs", "description": "Job posts: create (AI-generated JD), list, edit, publish, screen." },
    { "name": "Screenings", "description": "Screening runs and ranked, evidence-cited results." },
    { "name": "Applications", "description": "Applications and per-candidate interview kits." },
    { "name": "Interview Kits", "description": "30-minute CV-grounded structured interview kits." },
    { "name": "CVs", "description": "Bulk CV intake." },
    { "name": "Runs", "description": "Asynchronous background run status and pipeline progress." },
    { "name": "Usage", "description": "Credit balance and daily usage." },
    { "name": "Webhooks", "description": "Webhook endpoint registration." }
  ],
  "paths": {
    "/v1/jobs": {
      "post": {
        "tags": ["Jobs"],
        "operationId": "createJob",
        "summary": "Create a job post (async JD generation)",
        "description": "Creates a draft job post from minimal inputs. With auto_generate (default), a free jd_generate run starts immediately and writes the description (English, plus Arabic MSA when bilingual), SEO metadata, JSON-LD, suggested knockout questions, and a screening rubric. Returns 202 with the draft job and the run_id. With an Idempotency-Key header, a repeat submission returns the original job with 200 and replayed: true.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string" },
            "description": "Unique key per logical creation. Replays return the existing job post and run without creating duplicates. Requires auto_generate (the default)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateJobRequest" },
              "example": {
                "title": "Fleet Operations Manager",
                "seniority": "senior",
                "years_experience_min": 5,
                "location": "Dubai, UAE",
                "location_type": "onsite",
                "employment_type": "full_time",
                "salary_min": 18000,
                "salary_max": 25000,
                "salary_currency": "AED",
                "salary_period": "month",
                "skills": ["fleet management", "route optimization", "SAP"],
                "bilingual": true
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Draft created; JD generation started.",
            "headers": {
              "x-ratelimit-limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "x-ratelimit-remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "x-ratelimit-reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobAck" },
                "example": {
                  "job": {
                    "id": "4d1a8f36-9e02-47b8-b7c4-3a92d5e60f18",
                    "object": "job",
                    "status": "draft",
                    "slug": "fleet-operations-manager-9f2c1a",
                    "title": "Fleet Operations Manager",
                    "location": "Dubai, UAE",
                    "location_type": "onsite",
                    "employment_type": "full_time",
                    "seniority": "senior",
                    "language": "en",
                    "description_md": "",
                    "rubric": {},
                    "created_at": "2026-07-06T09:41:12.000Z"
                  },
                  "run_id": "0c9be9a4-2b77-4e19-8a5d-f21e6b40c7d3",
                  "replayed": false
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay — the original job post and run for this Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobAck" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      },
      "get": {
        "tags": ["Jobs"],
        "operationId": "listJobs",
        "summary": "List job posts",
        "description": "Most recent job posts for your organization, newest first.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["draft", "published", "paused", "closed", "filled"]
            },
            "description": "Filter by lifecycle status."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 20, "maximum": 50 },
            "description": "Number of job posts to return, capped at 50."
          }
        ],
        "responses": {
          "200": {
            "description": "Job post summaries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["object", "data"],
                  "properties": {
                    "object": { "type": "string", "const": "list" },
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/JobPostSummary" }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "tags": ["Jobs"],
        "operationId": "getJob",
        "summary": "Retrieve a job post",
        "description": "The complete job post: JD markdown (EN + AR when bilingual), SEO metadata, knockout questions, screening rubric, and application/screening counts.",
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": {
          "200": {
            "description": "The job post.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobPost" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      },
      "patch": {
        "tags": ["Jobs"],
        "operationId": "updateJob",
        "summary": "Edit a job post",
        "description": "Update the JD markdown, the screening rubric (criteria weights are integers 1-10, normalized at scoring time), the knockout questions, or the closing date. Editing rubric weights does not re-run screening — completed screenings keep their frozen rubric snapshot; re-ranking with new weights is instant in the dashboard.",
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UpdateJobRequest" },
              "example": {
                "rubric": {
                  "criteria": [
                    { "id": "fleet", "name": "Fleet management experience", "description": "Managed 50+ vehicle fleets", "weight": 5 },
                    { "id": "systems", "name": "Systems (SAP/TMS)", "description": "Hands-on ERP/TMS usage", "weight": 3 },
                    { "id": "leadership", "name": "Team leadership", "description": "Led ops teams of 5+", "weight": 2 }
                  ]
                },
                "closes_at": "2026-09-30T23:59:59Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated job post.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobPost" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/jobs/{id}/publish": {
      "post": {
        "tags": ["Jobs"],
        "operationId": "publishJob",
        "summary": "Publish a job post",
        "description": "Sets the job live (career page, Google for Jobs JSON-LD, board feeds) and starts a free distribute run. Enforces the plan's active-job limit; exceeding it returns 403 with code job_limit. Publishing an already-published job re-runs distribution.",
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": {
          "200": {
            "description": "Published; distribution started.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublishAck" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "The plan's active-job limit is reached.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
                "example": {
                  "error": {
                    "code": "job_limit",
                    "message": "Your free plan allows 1 active job post. Close or pause a job, or upgrade your plan."
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/jobs/{id}/screen": {
      "post": {
        "tags": ["Jobs"],
        "operationId": "screenJob",
        "summary": "Screen every CV (async run)",
        "description": "Screens every eligible application (has a CV, not withdrawn/rejected) against the job's rubric: anonymize → per-criterion scoring → evidence quotes → rank → top-K. Charges 1 credit per CV (deep mode: 3) up front, refunded automatically if the run fails. The rubric is frozen into the screening as a snapshot. Screening never rejects anyone — it recommends; humans disposition.",
        "parameters": [
          { "$ref": "#/components/parameters/Id" },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string" },
            "description": "Replays return the original screening and run without a second charge."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ScreenRequest" },
              "example": { "k": 5, "mode": "standard" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Screening accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScreenAck" },
                "example": {
                  "object": "screening",
                  "screening_id": "8a1c5e72-3f90-4b6d-a2e8-51c7d0b94f36",
                  "run_id": "5b8f0d21-6a3e-4c97-b1d0-84e7f2a9c655",
                  "k": 5,
                  "mode": "standard",
                  "candidates": 87,
                  "credits_charged": 87,
                  "replayed": false
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay — the original screening for this Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ScreenAck" }
              }
            }
          },
          "400": {
            "description": "Validation failed, no rubric on the job (missing_rubric), or no eligible candidates (no_eligible_candidates).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
                "example": {
                  "error": {
                    "code": "no_eligible_candidates",
                    "message": "No applications with a CV are eligible for screening on this job."
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/jobs/{id}/applications": {
      "get": {
        "tags": ["Applications"],
        "operationId": "listJobApplications",
        "summary": "List a job's applications",
        "description": "Applications for a job post, newest first, each with candidate details and its result from the latest screening (score, verdict, rank, in_top_k) when one exists.",
        "parameters": [
          { "$ref": "#/components/parameters/Id" },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": ["new", "screening", "shortlisted", "in_review", "interview", "offer", "hired", "rejected", "withdrawn"]
            },
            "description": "Filter by application status."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 50, "maximum": 100 },
            "description": "Number of applications to return, capped at 100."
          }
        ],
        "responses": {
          "200": {
            "description": "Applications.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["object", "data"],
                  "properties": {
                    "object": { "type": "string", "const": "list" },
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Application" }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/screenings/{id}": {
      "get": {
        "tags": ["Screenings"],
        "operationId": "getScreening",
        "summary": "Retrieve a screening",
        "description": "A screening run with its frozen rubric snapshot and model versions. Add ?include=results for the ranked per-candidate results: per-criterion scores with reasoning, verbatim evidence quotes (validated against the CV text; valid=false marks quotes that could not be matched), verdicts, red flags, and fraud signals.",
        "parameters": [
          { "$ref": "#/components/parameters/Id" },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["results"] },
            "description": "include=results embeds the ranked results array."
          }
        ],
        "responses": {
          "200": {
            "description": "The screening.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Screening" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/applications/{id}/interview-kit": {
      "post": {
        "tags": ["Interview Kits"],
        "operationId": "createInterviewKit",
        "summary": "Generate an interview kit (async run)",
        "description": "Generates a 30-minute structured interview kit grounded in this candidate's actual CV: easy/medium/hard/scenario/CV-based/job-based questions with model answers, scoring rubrics, red flags, and \"claims to probe\" anchored to specific CV quotes. Costs 12 credits, refunded on failure. Requires the application to have a CV.",
        "parameters": [
          { "$ref": "#/components/parameters/Id" },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string" },
            "description": "Replays return the original kit and run without a second charge."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InterviewKitRequest" },
              "example": { "language": "en" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Kit generation accepted.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/KitAck" },
                "example": {
                  "object": "interview_kit",
                  "kit_id": "c7e2a940-8b13-4f6d-95a1-2d80e4b7c358",
                  "run_id": "9f4b2c81-5e07-4a3d-b6c9-70e1d8a52f43",
                  "language": "en",
                  "credits_charged": 12,
                  "replayed": false
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay — the original kit for this Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/KitAck" }
              }
            }
          },
          "400": {
            "description": "Validation failed, or the application has no CV (no_cv).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
                "example": {
                  "error": { "code": "no_cv", "message": "This application has no CV to ground the kit in." }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/interview-kits/{id}": {
      "get": {
        "tags": ["Interview Kits"],
        "operationId": "getInterviewKit",
        "summary": "Retrieve an interview kit",
        "description": "The generated kit. content is {} while status is \"generating\" — poll the run or wait for the job.succeeded webhook.",
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": {
          "200": {
            "description": "The kit.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/InterviewKit" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/cvs": {
      "post": {
        "tags": ["CVs"],
        "operationId": "uploadCvs",
        "summary": "Bulk CV intake",
        "description": "Attach up to 10 base64-encoded CVs (.pdf or .docx, ≤10 MB decoded each) to a job. Each file becomes a candidate + application with source \"import\". Free — parsing and scoring happen during screening, so nothing is charged until POST /v1/jobs/{id}/screen.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CvUploadRequest" },
              "example": {
                "job_id": "4d1a8f36-9e02-47b8-b7c4-3a92d5e60f18",
                "files": [
                  { "filename": "jane-doe.pdf", "content_base64": "JVBERi0xLjQK…" }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Applications created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["object", "data"],
                  "properties": {
                    "object": { "type": "string", "const": "list" },
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/CvUploadResult" }
                    }
                  }
                },
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "application_id": "e2c81f47-0a95-4d36-b8e1-6f24a0d7c593",
                      "candidate_id": "1b7d4e90-3c58-4f2a-96d0-84b5e1a2c766",
                      "cv_file_id": "7f3a2d15-9e60-4b8c-a4f7-02c9d6e18b54",
                      "filename": "jane-doe.pdf"
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/runs/{id}": {
      "get": {
        "tags": ["Runs"],
        "operationId": "getRun",
        "summary": "Retrieve a run",
        "description": "Background run status with live per-stage pipeline progress. On success, result carries the run-type-specific payload (jd_generate: job_post_id; screen_batch: screening_id, screened; interview_kit: kit_id; distribute: channels).",
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": {
          "200": {
            "description": "The run.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Run" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": ["Usage"],
        "operationId": "getUsage",
        "summary": "Credit balance and daily usage",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "default": 30, "maximum": 90 },
            "description": "Reporting window in days, capped at 90."
          }
        ],
        "responses": {
          "200": {
            "description": "Usage report.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UsageReport" },
                "example": {
                  "object": "usage",
                  "days": 7,
                  "credit_balance": 1418,
                  "daily": [
                    {
                      "day": "2026-07-01",
                      "credits": 99,
                      "requests": 4,
                      "tokens_in": 412650,
                      "tokens_out": 58210
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    },
    "/v1/webhooks": {
      "post": {
        "tags": ["Webhooks"],
        "operationId": "createWebhookEndpoint",
        "summary": "Register a webhook endpoint",
        "description": "The whsec_ signing secret is returned only in this response. Deliveries follow the Standard Webhooks spec: headers webhook-id, webhook-timestamp, webhook-signature = \"v1,\" + base64(HMAC-SHA256(base64decode(secret without whsec_), \"{id}.{timestamp}.{raw body}\")).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateWebhookRequest" },
              "example": {
                "url": "https://api.your-app.com/hooks/hire",
                "events": ["job.succeeded", "job.failed", "application.received", "screening.completed"]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Endpoint registered.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WebhookEndpointCreated" },
                "example": {
                  "id": "a4f81c2d-6e93-40b7-95d2-3c08e7b1f649",
                  "object": "webhook_endpoint",
                  "url": "https://api.your-app.com/hooks/hire",
                  "events": ["job.succeeded", "job.failed", "application.received", "screening.completed"],
                  "secret": "whsec_Zks3JprXcO1FaK9yTqR2v8wBnE5dLmHu"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      },
      "get": {
        "tags": ["Webhooks"],
        "operationId": "listWebhookEndpoints",
        "summary": "List webhook endpoints",
        "description": "Signing secrets are never included in listings.",
        "responses": {
          "200": {
            "description": "Registered endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["object", "data"],
                  "properties": {
                    "object": { "type": "string", "const": "list" },
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/WebhookEndpointSummary" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/InternalError" }
        }
      }
    }
  },
  "webhooks": {
    "job.succeeded": {
      "post": {
        "summary": "A run finished successfully",
        "description": "Sent to every active endpoint subscribed to job.succeeded when any background run (jd_generate, screen_batch, interview_kit, distribute) succeeds. Verify the webhook-signature header before trusting the payload.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RunWebhookPayload" }
            }
          }
        },
        "responses": {
          "200": { "description": "Return any 2xx to acknowledge receipt." }
        }
      }
    },
    "job.failed": {
      "post": {
        "summary": "A run failed",
        "description": "Sent after the run is marked failed and its credits are refunded. data.error carries the failure reason.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RunWebhookPayload" }
            }
          }
        },
        "responses": {
          "200": { "description": "Return any 2xx to acknowledge receipt." }
        }
      }
    },
    "application.received": {
      "post": {
        "summary": "A new application arrived",
        "description": "Sent when a candidate applies through the career page, embed widget, or apply API. data carries application and candidate identifiers plus the job post ID.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/EventWebhookPayload" }
            }
          }
        },
        "responses": {
          "200": { "description": "Return any 2xx to acknowledge receipt." }
        }
      }
    },
    "screening.completed": {
      "post": {
        "summary": "A screening finished",
        "description": "Sent when a screen_batch run completes and ranked results are available. data carries the screening_id and job post ID — fetch GET /v1/screenings/{id}?include=results for the ranked list.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/EventWebhookPayload" }
            }
          }
        },
        "responses": {
          "200": { "description": "Return any 2xx to acknowledge receipt." }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key from Dashboard → API Keys. Format: wz_live_… (production) or wz_test_… (development). Send as: Authorization: Bearer wz_live_…"
      }
    },
    "parameters": {
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Resource ID (UUID)."
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "The key's per-minute request budget. Present on every authenticated response.",
        "schema": { "type": "integer" }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current 60-second window.",
        "schema": { "type": "integer" }
      },
      "RateLimitReset": {
        "description": "Unix seconds when the current window resets.",
        "schema": { "type": "integer" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Validation failed. The message lists each violation as \"field: problem\" joined with \"; \".",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
            "example": {
              "error": {
                "code": "invalid_request",
                "message": "title: String must contain at least 2 character(s)"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, unknown, or revoked API key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
            "example": {
              "error": {
                "code": "invalid_api_key",
                "message": "Missing or invalid API key. Pass it as: Authorization: Bearer wz_live_..."
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Insufficient credits. Nothing was charged.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
            "example": {
              "error": {
                "code": "insufficient_credits",
                "message": "Insufficient credits: need 87, have 12. Top up or upgrade your plan."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource doesn't exist or belongs to another organization.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
            "example": {
              "error": { "code": "not_found", "message": "Job not found" }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-key request budget exhausted for the current 60-second window. Wait retry-after seconds.",
        "headers": {
          "retry-after": {
            "schema": { "type": "integer" },
            "description": "Seconds until the window resets (minimum 1)."
          },
          "x-ratelimit-limit": { "schema": { "type": "integer" } },
          "x-ratelimit-remaining": { "schema": { "type": "integer" } },
          "x-ratelimit-reset": {
            "schema": { "type": "integer" },
            "description": "Unix seconds when the window resets."
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
            "example": {
              "error": {
                "code": "rate_limit_exceeded",
                "message": "Too many requests. Back off and retry."
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server error. Safe to retry with backoff.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorEnvelope" },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "Something went wrong on our side."
              }
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code: invalid_request, invalid_api_key, insufficient_credits, job_limit, missing_rubric, no_eligible_candidates, no_cv, not_found, rate_limit_exceeded, internal_error.",
                "examples": ["invalid_request"]
              },
              "message": { "type": "string" }
            }
          }
        }
      },
      "RubricCriterion": {
        "type": "object",
        "required": ["id", "name", "weight"],
        "properties": {
          "id": { "type": "string", "maxLength": 60 },
          "name": { "type": "string", "maxLength": 120 },
          "description": { "type": "string", "maxLength": 600, "default": "" },
          "weight": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "description": "Relative weight 1-10; normalized to fractions at scoring time."
          }
        }
      },
      "Rubric": {
        "type": "object",
        "required": ["criteria"],
        "properties": {
          "criteria": {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": { "$ref": "#/components/schemas/RubricCriterion" }
          }
        }
      },
      "KnockoutQuestion": {
        "type": "object",
        "required": ["id", "question", "type"],
        "properties": {
          "id": { "type": "string", "maxLength": 60 },
          "question": { "type": "string", "maxLength": 500 },
          "type": { "type": "string", "enum": ["boolean", "text", "number"] },
          "disqualifyOn": {
            "description": "Answer value that disqualifies (evaluated inline at apply time, no AI)."
          }
        }
      },
      "CreateJobRequest": {
        "type": "object",
        "required": ["title"],
        "properties": {
          "title": { "type": "string", "minLength": 2, "maxLength": 200 },
          "seniority": {
            "type": "string",
            "enum": ["junior", "mid", "senior", "lead", "director", "executive"]
          },
          "years_experience_min": { "type": "integer", "minimum": 0, "maximum": 60 },
          "years_experience_max": { "type": "integer", "minimum": 0, "maximum": 60 },
          "location": { "type": "string", "maxLength": 200 },
          "location_type": {
            "type": "string",
            "enum": ["onsite", "remote", "hybrid"],
            "default": "onsite"
          },
          "employment_type": {
            "type": "string",
            "enum": ["full_time", "part_time", "contract", "internship"],
            "default": "full_time"
          },
          "salary_min": { "type": "integer", "minimum": 0 },
          "salary_max": { "type": "integer", "minimum": 0 },
          "salary_currency": { "type": "string", "minLength": 3, "maxLength": 3, "default": "USD" },
          "salary_period": { "type": "string", "enum": ["month", "year", "hour"], "default": "month" },
          "salary_visible": {
            "type": "boolean",
            "default": false,
            "description": "Show the salary band on the public job page."
          },
          "skills": {
            "type": "array",
            "items": { "type": "string", "maxLength": 80 },
            "maxItems": 30,
            "default": []
          },
          "language": { "type": "string", "maxLength": 12, "default": "en" },
          "bilingual": {
            "type": "boolean",
            "default": false,
            "description": "Also generate the Arabic (MSA) JD."
          },
          "extra_notes": { "type": "string", "maxLength": 2000 },
          "auto_generate": {
            "type": "boolean",
            "default": true,
            "description": "Start a free jd_generate run immediately. When false, the draft is created empty and run_id is null."
          }
        }
      },
      "UpdateJobRequest": {
        "type": "object",
        "description": "At least one field is required.",
        "properties": {
          "description_md": { "type": "string", "maxLength": 50000 },
          "rubric": { "$ref": "#/components/schemas/Rubric" },
          "knockout_questions": {
            "type": "array",
            "maxItems": 20,
            "items": { "$ref": "#/components/schemas/KnockoutQuestion" }
          },
          "closes_at": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "JobPosting validThrough. null clears it."
          }
        }
      },
      "JobPostSummary": {
        "type": "object",
        "required": ["id", "object", "status", "slug", "title", "location_type", "employment_type", "language", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "const": "job" },
          "status": {
            "type": "string",
            "enum": ["draft", "published", "paused", "closed", "filled"]
          },
          "slug": { "type": "string" },
          "title": { "type": "string" },
          "department": { "type": ["string", "null"] },
          "location": { "type": ["string", "null"] },
          "location_type": { "type": "string", "enum": ["onsite", "remote", "hybrid"] },
          "employment_type": {
            "type": "string",
            "enum": ["full_time", "part_time", "contract", "internship"]
          },
          "seniority": { "type": ["string", "null"] },
          "language": { "type": "string" },
          "published_at": { "type": ["string", "null"], "format": "date-time" },
          "closes_at": { "type": ["string", "null"], "format": "date-time" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "JobPost": {
        "allOf": [
          { "$ref": "#/components/schemas/JobPostSummary" },
          {
            "type": "object",
            "properties": {
              "years_experience_min": { "type": ["integer", "null"] },
              "years_experience_max": { "type": ["integer", "null"] },
              "salary_min": { "type": ["integer", "null"] },
              "salary_max": { "type": ["integer", "null"] },
              "salary_currency": { "type": ["string", "null"] },
              "salary_period": { "type": ["string", "null"] },
              "salary_visible": { "type": "boolean" },
              "skills": { "type": "array", "items": { "type": "string" } },
              "description_md": {
                "type": "string",
                "description": "English JD (markdown). Empty until the jd_generate run finishes."
              },
              "description_ar_md": {
                "type": ["string", "null"],
                "description": "Arabic (MSA) JD when bilingual generation was requested."
              },
              "jd_meta": {
                "type": "object",
                "additionalProperties": true,
                "description": "{metaTitle, metaDescription} for the public page."
              },
              "knockout_questions": {
                "type": "array",
                "items": { "$ref": "#/components/schemas/KnockoutQuestion" }
              },
              "rubric": {
                "description": "The screening rubric ({criteria: […]}); {} until generated or set.",
                "type": "object",
                "additionalProperties": true
              },
              "counts": {
                "type": "object",
                "description": "Present on detail reads.",
                "properties": {
                  "applications": { "type": "integer" },
                  "screenings": { "type": "integer" }
                }
              }
            }
          }
        ]
      },
      "JobAck": {
        "type": "object",
        "required": ["job", "run_id", "replayed"],
        "properties": {
          "job": { "$ref": "#/components/schemas/JobPost" },
          "run_id": {
            "type": ["string", "null"],
            "description": "The jd_generate run; null when auto_generate was false."
          },
          "replayed": {
            "type": "boolean",
            "description": "true when an Idempotency-Key matched an existing creation."
          }
        }
      },
      "PublishAck": {
        "type": "object",
        "required": ["job", "run_id"],
        "properties": {
          "job": { "$ref": "#/components/schemas/JobPost" },
          "run_id": { "type": "string", "description": "The free distribute run." }
        }
      },
      "ScreenRequest": {
        "type": "object",
        "properties": {
          "k": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 10,
            "description": "How many top candidates to surface (in_top_k)."
          },
          "mode": {
            "type": "string",
            "enum": ["standard", "deep"],
            "default": "standard",
            "description": "standard = 1 credit/CV, evidence pass on the top slice; deep = 3 credits/CV, evidence pass on every CV."
          }
        }
      },
      "ScreenAck": {
        "type": "object",
        "required": ["object", "screening_id", "run_id", "k", "mode", "candidates", "credits_charged", "replayed"],
        "properties": {
          "object": { "type": "string", "const": "screening" },
          "screening_id": { "type": "string" },
          "run_id": { "type": "string" },
          "k": { "type": "integer" },
          "mode": { "type": "string", "enum": ["standard", "deep"] },
          "candidates": { "type": "integer", "description": "Eligible CVs queued for screening." },
          "credits_charged": { "type": "integer" },
          "replayed": { "type": "boolean" }
        }
      },
      "CriterionScore": {
        "type": "object",
        "required": ["criterionId", "name", "score", "weight", "reasoning"],
        "properties": {
          "criterionId": { "type": "string" },
          "name": { "type": "string" },
          "score": { "type": "number", "minimum": 0, "maximum": 100 },
          "weight": { "type": "integer", "description": "The rubric weight (1-10) used for this run." },
          "reasoning": { "type": "string" }
        }
      },
      "Evidence": {
        "type": "object",
        "required": ["criterionId", "quote", "valid"],
        "properties": {
          "criterionId": { "type": "string" },
          "quote": {
            "type": "string",
            "description": "Verbatim quote from the anonymized CV text."
          },
          "page": { "type": ["integer", "null"] },
          "valid": {
            "type": "boolean",
            "description": "true only when the quote was found verbatim (whitespace-normalized) in the CV text. Invalid quotes are kept and flagged, never silently dropped."
          }
        }
      },
      "ScreeningResult": {
        "type": "object",
        "required": ["application_id", "candidate", "score", "verdict", "rank", "in_top_k", "criterion_scores", "evidence"],
        "properties": {
          "application_id": { "type": "string" },
          "candidate": {
            "type": "object",
            "required": ["id", "full_name"],
            "properties": {
              "id": { "type": "string" },
              "full_name": { "type": "string" }
            }
          },
          "score": {
            "type": "number",
            "description": "Weighted 0-100 overall — the weighted average of criterion scores."
          },
          "verdict": { "type": "string", "enum": ["strong", "good", "partial", "weak"] },
          "rank": { "type": ["integer", "null"], "description": "1 = best." },
          "in_top_k": {
            "type": "boolean",
            "description": "Recommendation only — humans disposition candidates."
          },
          "criterion_scores": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CriterionScore" }
          },
          "evidence": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Evidence" }
          },
          "red_flags": { "type": "array", "items": {} },
          "missing_info": { "type": "array", "items": {} },
          "fraud_signals": {
            "type": "array",
            "items": {},
            "description": "Dedupe / fabrication / AI-flood signals: [{type, detail, severity}]."
          }
        }
      },
      "Screening": {
        "type": "object",
        "required": ["id", "object", "job_post_id", "status", "k", "mode", "rubric", "total_candidates", "screened_count", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "const": "screening" },
          "job_post_id": { "type": "string" },
          "run_id": { "type": ["string", "null"] },
          "status": { "type": "string", "enum": ["queued", "processing", "succeeded", "failed"] },
          "k": { "type": "integer" },
          "mode": { "type": "string", "enum": ["standard", "deep"] },
          "rubric": {
            "type": "object",
            "additionalProperties": true,
            "description": "Frozen rubric snapshot used for this run."
          },
          "model_versions": {
            "type": "object",
            "additionalProperties": true,
            "description": "Model IDs used per stage ({parse, score, evidence}) — compliance record."
          },
          "total_candidates": { "type": "integer" },
          "screened_count": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" },
          "finished_at": { "type": ["string", "null"], "format": "date-time" },
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ScreeningResult" },
            "description": "Present with ?include=results, ranked best-first."
          }
        }
      },
      "Application": {
        "type": "object",
        "required": ["id", "object", "status", "source", "candidate", "latest_screening", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "const": "application" },
          "status": {
            "type": "string",
            "enum": ["new", "screening", "shortlisted", "in_review", "interview", "offer", "hired", "rejected", "withdrawn"]
          },
          "source": {
            "type": "string",
            "description": "career_page | embed | upload | email_in | import | other."
          },
          "knockout_passed": { "type": ["boolean", "null"] },
          "cv_file_id": { "type": ["string", "null"] },
          "candidate": {
            "type": "object",
            "required": ["id", "full_name"],
            "properties": {
              "id": { "type": "string" },
              "full_name": { "type": "string" },
              "email": { "type": ["string", "null"] },
              "phone": { "type": ["string", "null"] },
              "location": { "type": ["string", "null"] }
            }
          },
          "latest_screening": {
            "type": ["object", "null"],
            "description": "Result from the most recent screening of this job, when one exists.",
            "properties": {
              "screening_id": { "type": "string" },
              "score": { "type": "number" },
              "verdict": { "type": "string", "enum": ["strong", "good", "partial", "weak"] },
              "rank": { "type": ["integer", "null"] },
              "in_top_k": { "type": "boolean" }
            }
          },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "InterviewKitRequest": {
        "type": "object",
        "properties": {
          "language": {
            "type": "string",
            "minLength": 2,
            "maxLength": 12,
            "description": "Kit language. Defaults to the job post's language."
          }
        }
      },
      "KitAck": {
        "type": "object",
        "required": ["object", "kit_id", "run_id", "language", "credits_charged", "replayed"],
        "properties": {
          "object": { "type": "string", "const": "interview_kit" },
          "kit_id": { "type": "string" },
          "run_id": { "type": "string" },
          "language": { "type": "string" },
          "credits_charged": { "type": "integer" },
          "replayed": { "type": "boolean" }
        }
      },
      "KitQuestion": {
        "type": "object",
        "required": ["question", "modelAnswer", "rubric", "redFlags"],
        "properties": {
          "question": { "type": "string" },
          "modelAnswer": { "type": "string" },
          "rubric": { "type": "string", "description": "What a 1/3/5 answer looks like." },
          "redFlags": { "type": "array", "items": { "type": "string" } }
        }
      },
      "KitContent": {
        "type": "object",
        "required": ["durationMinutes", "plan", "sections", "probes"],
        "properties": {
          "durationMinutes": { "type": "integer", "const": 30 },
          "plan": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["minutes", "section"],
              "properties": {
                "minutes": { "type": "integer" },
                "section": { "type": "string" }
              }
            }
          },
          "sections": {
            "type": "object",
            "description": "Question banks keyed by section: easy, medium, hard, scenario, cv_based, job_based.",
            "properties": {
              "easy": { "type": "array", "items": { "$ref": "#/components/schemas/KitQuestion" } },
              "medium": { "type": "array", "items": { "$ref": "#/components/schemas/KitQuestion" } },
              "hard": { "type": "array", "items": { "$ref": "#/components/schemas/KitQuestion" } },
              "scenario": { "type": "array", "items": { "$ref": "#/components/schemas/KitQuestion" } },
              "cv_based": { "type": "array", "items": { "$ref": "#/components/schemas/KitQuestion" } },
              "job_based": { "type": "array", "items": { "$ref": "#/components/schemas/KitQuestion" } }
            }
          },
          "probes": {
            "type": "array",
            "description": "CV claims worth pressure-testing, each anchored to a verbatim quote.",
            "items": {
              "type": "object",
              "required": ["cvClaim", "quote", "question"],
              "properties": {
                "cvClaim": { "type": "string" },
                "quote": { "type": "string" },
                "question": { "type": "string" }
              }
            }
          }
        }
      },
      "InterviewKit": {
        "type": "object",
        "required": ["id", "object", "status", "application_id", "job_post_id", "language", "content", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "const": "interview_kit" },
          "status": { "type": "string", "enum": ["generating", "ready", "error"] },
          "application_id": { "type": "string" },
          "job_post_id": { "type": "string" },
          "run_id": { "type": ["string", "null"] },
          "language": { "type": "string" },
          "content": {
            "description": "The kit ({} while status is \"generating\").",
            "oneOf": [
              { "$ref": "#/components/schemas/KitContent" },
              { "type": "object", "additionalProperties": false }
            ]
          },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "CvUploadRequest": {
        "type": "object",
        "required": ["job_id", "files"],
        "properties": {
          "job_id": { "type": "string" },
          "files": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "items": {
              "type": "object",
              "required": ["filename", "content_base64"],
              "properties": {
                "filename": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Must end in .pdf or .docx."
                },
                "content_base64": {
                  "type": "string",
                  "description": "Raw base64 (no data: URI prefix). ≤10 MB decoded."
                }
              }
            }
          }
        }
      },
      "CvUploadResult": {
        "type": "object",
        "required": ["application_id", "candidate_id", "cv_file_id", "filename"],
        "properties": {
          "application_id": { "type": "string" },
          "candidate_id": { "type": "string" },
          "cv_file_id": { "type": "string" },
          "filename": { "type": "string" }
        }
      },
      "RunStage": {
        "type": "object",
        "required": ["key", "label", "status"],
        "properties": {
          "key": { "type": "string" },
          "label": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "running", "done", "error"] },
          "startedAt": { "type": "string", "format": "date-time" },
          "endedAt": { "type": "string", "format": "date-time" },
          "meta": { "type": "object", "additionalProperties": true }
        }
      },
      "Run": {
        "type": "object",
        "required": ["id", "object", "type", "status", "stages", "result", "credits_charged", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "const": "run" },
          "type": {
            "type": "string",
            "enum": ["jd_generate", "screen_batch", "interview_kit", "distribute", "ingest_company", "import_ats"]
          },
          "status": {
            "type": "string",
            "enum": ["queued", "processing", "succeeded", "failed", "canceled"]
          },
          "stage": {
            "type": ["string", "null"],
            "description": "Key of the currently (or last) running stage."
          },
          "stages": { "type": "array", "items": { "$ref": "#/components/schemas/RunStage" } },
          "result": {
            "type": "object",
            "additionalProperties": true,
            "description": "Empty until success. jd_generate: {job_post_id}. screen_batch: {screening_id, screened}. interview_kit: {kit_id}. distribute: {channels}."
          },
          "error": { "type": ["string", "null"] },
          "credits_charged": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" },
          "finished_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "UsageReport": {
        "type": "object",
        "required": ["object", "days", "credit_balance", "daily"],
        "properties": {
          "object": { "type": "string", "const": "usage" },
          "days": { "type": "integer" },
          "credit_balance": { "type": "integer" },
          "daily": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["day", "credits", "requests", "tokens_in", "tokens_out"],
              "properties": {
                "day": { "type": "string", "format": "date" },
                "credits": { "type": "integer" },
                "requests": { "type": "integer" },
                "tokens_in": { "type": "integer" },
                "tokens_out": { "type": "integer" }
              }
            }
          }
        }
      },
      "WebhookEvent": {
        "type": "string",
        "enum": ["job.succeeded", "job.failed", "application.received", "screening.completed"]
      },
      "CreateWebhookRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Must be https in production."
          },
          "events": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/WebhookEvent" },
            "default": ["job.succeeded", "job.failed", "application.received", "screening.completed"]
          }
        }
      },
      "WebhookEndpointCreated": {
        "type": "object",
        "required": ["id", "object", "url", "events", "secret"],
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "const": "webhook_endpoint" },
          "url": { "type": "string" },
          "events": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/WebhookEvent" }
          },
          "secret": {
            "type": "string",
            "description": "whsec_ signing secret — returned only in this response."
          }
        }
      },
      "WebhookEndpointSummary": {
        "type": "object",
        "required": ["id", "url", "events", "active", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "url": { "type": "string" },
          "events": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/WebhookEvent" }
          },
          "active": { "type": "boolean" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "RunWebhookPayload": {
        "type": "object",
        "required": ["event", "data"],
        "properties": {
          "event": { "type": "string", "enum": ["job.succeeded", "job.failed"] },
          "data": {
            "type": "object",
            "description": "The run payload: id, type, status, result, error, credits_charged, created_at, finished_at.",
            "required": ["id", "type", "status", "result", "credits_charged", "created_at"],
            "properties": {
              "id": { "type": "string" },
              "type": {
                "type": "string",
                "enum": ["jd_generate", "screen_batch", "interview_kit", "distribute", "ingest_company", "import_ats"]
              },
              "status": { "type": "string", "enum": ["succeeded", "failed"] },
              "result": { "type": "object", "additionalProperties": true },
              "error": { "type": ["string", "null"] },
              "credits_charged": { "type": "integer" },
              "created_at": { "type": "string", "format": "date-time" },
              "finished_at": { "type": ["string", "null"], "format": "date-time" }
            }
          }
        }
      },
      "EventWebhookPayload": {
        "type": "object",
        "required": ["event", "data"],
        "properties": {
          "event": { "type": "string", "enum": ["application.received", "screening.completed"] },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "application.received: {application_id, job_post_id, candidate_id, source}. screening.completed: {screening_id, job_post_id, k, screened}."
          }
        }
      }
    }
  }
}
