{"openapi":"3.1.0","info":{"title":"Evidal Hiring Manager API","version":"1.0.0","summary":"Public REST surface used by the hiring-manager MCP server and by any tool or AI agent calling Evidal directly.","description":"The Evidal hiring-manager API lets a hiring manager — or an AI agent acting\non their behalf — manage roles, review candidates, configure Slack\nnotifications, and kick off Stripe billing flows. Every endpoint below is\nauthenticated with a company-scoped Bearer API key of the form\n`evidal_sk_...`.\n\nGenerate or rotate your API key from\n`https://app.evidal.ai/admin/{company}/integrations`.\n\nRate limit: **100 requests/hour per API key**. Exceeding the limit\nreturns `429 Too Many Requests`.\n\nSome admin-only flows (adding a payment method, opening the Stripe billing\nportal) require an authenticated browser session and are NOT exposed on\nthis API; use the admin dashboard for those.\n"},"servers":[{"url":"https://app.evidal.ai","description":"Production"}],"security":[{"BearerApiKey":[]}],"tags":[{"name":"Company","description":"Read/update the company record."},{"name":"Roles","description":"Role management (CRUD, lifecycle)."},{"name":"Candidates","description":"Candidate review + hiring decisions. PII is gated."},{"name":"Slack","description":"Notification routing."},{"name":"API key","description":"API key rotation."},{"name":"DNS","description":"Custom-domain verification status."},{"name":"Billing","description":"Stripe Checkout + per-role billing state."},{"name":"Members","description":"Hiring-team member management (invite, list, promote/demote, remove, revoke invite)."},{"name":"Onboarding","description":"Unauthenticated company self-serve signup. A caller POSTs to `/start` with\nthe owner's email + desired company name, receives a `session_id`, and\nwaits for the owner to click the magic-link in the verification email.\nThe session can then be completed (creating the company + first API key)\nor cancelled. All onboarding endpoints are public; no Bearer key is\nrequired or accepted.\n"},{"name":"Candidate self-service","description":"Endpoints a candidate (or their AI agent) calls about their own data.\nAuthenticated with the candidate session — the `evi_cand_session`\ncookie set at magic-link sign-in, or `Authorization: Bearer\n<candidate-session JWT>` for API/mobile callers. Company API keys are\nNOT accepted on these endpoints.\n"}],"paths":{"/api/v1/company/{slug}":{"patch":{"tags":["Company"],"operationId":"updateCompany","summary":"Update company-level fields.","description":"Patch the company. Via API key auth, only `name`, `notification_email`,\n`description`, and `logo_url` are accepted. `domain` mutations are\nrejected — use the admin dashboard for DNS-verified domain changes.\n","parameters":[{"$ref":"#/components/parameters/Slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"notification_email":{"oneOf":[{"type":"string","format":"email"},{"type":"array","items":{"type":"string","format":"email"}}]},"description":{"type":"string","maxLength":5000},"logo_url":{"type":"string","format":"uri"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Updated company record.","content":{"application/json":{"schema":{"type":"object","properties":{"company":{"$ref":"#/components/schemas/Company"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/company/{slug}/roles":{"get":{"tags":["Roles"],"operationId":"listRoles","summary":"List roles for the company.","parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"include_inactive","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Roles list (active only by default).","content":{"application/json":{"schema":{"type":"object","properties":{"roles":{"type":"array","items":{"$ref":"#/components/schemas/Role"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/roles/create":{"post":{"tags":["Roles"],"operationId":"createRole","summary":"Create a new role.","description":"New roles always start in `trial` status with 3 trial evaluations.\nAfter creation, call `PATCH /roles/{role_id}` to set `must_haves`,\n`nice_to_haves`, or `custom_notes`.\n","parameters":[{"$ref":"#/components/parameters/Slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","level","ai_native_required"],"properties":{"title":{"type":"string","maxLength":200,"description":"Role title. Capped at 200 chars; a title that slugifies to empty (punctuation/unicode-only) is rejected with 422."},"level":{"type":"string"},"ai_native_required":{"type":"boolean"},"archetype_slug":{"type":"string"},"description":{"type":"string"},"weight_adjustments":{"type":"object","additionalProperties":{"type":"number"},"description":"Per-dimension weight overrides. Keys must be a subset of [grounding, causal, tradeoff, ownership, robustness]; each value must be a finite number in [0, 5]. A bad shape is rejected with 422."},"idempotency_key":{"type":"string","minLength":8,"maxLength":128,"description":"Optional key for safe retry. A repeat POST with the same key for this company returns the existing role (with idempotent_replay true) instead of creating a duplicate."}}}}}},"responses":{"201":{"description":"Created role. Returns the role record alongside its billing\nstatus, the number of trial evaluations still available, and the\nmachine-readable `skill.md` URL for the new role.\n","content":{"application/json":{"schema":{"type":"object","properties":{"role":{"$ref":"#/components/schemas/Role"},"role_status":{"type":"string","description":"Billing status of the new role. New roles start in `trial`.","example":"trial"},"trial_evals_remaining":{"type":"integer","description":"Trial evaluations still available (3 minus the role's eval_count).","example":3},"skill_md_url":{"type":"string","format":"uri","description":"Absolute URL serving the role's machine-readable skill.md."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"A role with the generated slug already exists for this company.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"slug":{"type":"string"}}}}}},"422":{"description":"Validation failure on a present field: title too long or\nunsluggable (empty/reserved), bad `weight_adjustments` shape, or a\nmalformed `idempotency_key`.\n","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/v1/company/{slug}/roles/{roleId}":{"get":{"tags":["Roles"],"operationId":"getRole","summary":"Get a single role by id or slug.","parameters":[{"$ref":"#/components/parameters/Slug"},{"$ref":"#/components/parameters/RoleId"}],"responses":{"200":{"description":"Role detail.","content":{"application/json":{"schema":{"type":"object","properties":{"role":{"$ref":"#/components/schemas/Role"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Roles"],"operationId":"updateRole","summary":"Patch role fields.","description":"Whitelisted fields: `title`, `description`, `level`,\n`ai_native_required`, `archetype_slug`, `weight_adjustments`,\n`must_haves`, `nice_to_haves`, `custom_notes`.\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"$ref":"#/components/parameters/RoleId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"level":{"type":"string"},"ai_native_required":{"type":"boolean"},"archetype_slug":{"type":"string"},"weight_adjustments":{"type":"object","additionalProperties":{"type":"number"}},"must_haves":{"type":"array","items":{"type":"string"}},"nice_to_haves":{"type":"array","items":{"type":"string"}},"custom_notes":{"type":"string"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Updated role.","content":{"application/json":{"schema":{"type":"object","properties":{"role":{"$ref":"#/components/schemas/Role"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/{slug}/roles/{roleId}/deactivate":{"post":{"tags":["Roles"],"operationId":"deactivateRole","summary":"Deactivate a role (stops accepting applications).","description":"Destructive. Sets `role.active = false`.","parameters":[{"$ref":"#/components/parameters/Slug"},{"$ref":"#/components/parameters/RoleId"}],"responses":{"200":{"description":"Role deactivated.","content":{"application/json":{"schema":{"type":"object","properties":{"role":{"$ref":"#/components/schemas/Role"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/{slug}/roles/{roleId}/active":{"post":{"tags":["Roles"],"operationId":"setRoleActive","summary":"Publish or unpublish a role (toggle role.active).","description":"Toggles `role.active`. This is the route the `publish_role` MCP tool\ncalls (with `{ active: true }`) to make a draft role visible to\ncandidates, and the inverse of `deactivate` for unpausing. Dual-auth:\nBearer API key or admin+ session cookie. Body `active` is required.\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"$ref":"#/components/parameters/RoleId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["active"],"properties":{"active":{"type":"boolean","description":"true publishes the role; false pauses it."}}}}}},"responses":{"200":{"description":"Role active flag updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"active":{"type":"boolean","description":"The role's new active state."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/{slug}/roles/{roleId}/activate-plan":{"post":{"tags":["Billing"],"operationId":"activateRolePlan","summary":"Activate a role plan using a stored payment method.","description":"One-click activation against a saved card. Returns\n`{ status: \"activated\" }` on success. If no payment method is on file\nor 3DS is required, returns\n`{ status: \"requires_setup\", checkout_url }` — the hiring manager\nmust open `checkout_url` in a browser to save a card first.\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"$ref":"#/components/parameters/RoleId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","enum":["full_access","pay_per_eval"]}}}}}},"responses":{"200":{"description":"Activation result.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"status":{"type":"string","enum":["activated"]}}},{"type":"object","properties":{"status":{"type":"string","enum":["requires_setup"]},"checkout_url":{"type":"string","format":"uri"}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/{slug}/candidates":{"get":{"tags":["Candidates"],"operationId":"listCandidates","summary":"List candidates (paginated, never includes PII).","description":"NEVER returns `display_name` or `contact_email`, regardless of a\ncandidate's lead-unlock state. Use `get_candidate` (GET\n/candidates/{candidateAgentId}) to retrieve PII for an individual\nunlocked candidate. The list/single physical disjointness prevents\nbulk-dump of PII via list responses.\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}},{"name":"unlocked_only","in":"query","required":false,"description":"When `true`, restricts results to candidates with a matching `lead_unlocks` row (candidates for whom the $100 lead unlock has been purchased).","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Paginated candidates.","content":{"application/json":{"schema":{"type":"object","properties":{"candidates":{"type":"array","items":{"$ref":"#/components/schemas/CandidateSummary"}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/candidates/{candidateAgentId}":{"get":{"tags":["Candidates"],"operationId":"getCandidate","summary":"Get a single candidate (PII-gated).","parameters":[{"$ref":"#/components/parameters/Slug"},{"$ref":"#/components/parameters/CandidateAgentId"}],"responses":{"200":{"description":"Candidate detail.","content":{"application/json":{"schema":{"type":"object","properties":{"candidate":{"$ref":"#/components/schemas/Candidate"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/{slug}/candidates/{candidateAgentId}/decision":{"post":{"tags":["Candidates"],"operationId":"recordDecision","summary":"Record a hiring decision for a candidate.","description":"Timestamped server-side. Idempotent: re-submitting the same decision\nthat is already recorded is a 200 no-op and the response carries\n`idempotent: true`.\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"$ref":"#/components/parameters/CandidateAgentId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decision"],"properties":{"decision":{"type":"string","enum":["advance","reject","hold"]}}}}}},"responses":{"200":{"description":"Decision recorded (or idempotent no-op when the same decision was\nalready on file). `idempotent` is present and `true` only on the\nno-op path.\n","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"decision":{"type":"string","enum":["advance","reject","hold"]},"candidate_agent_id":{"type":"string","format":"uuid"},"idempotent":{"type":"boolean","description":"Present and `true` only when the decision was already recorded (no state change)."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/{slug}/slack/configure":{"patch":{"tags":["Slack"],"operationId":"configureSlack","summary":"Configure Slack notification routing.","description":"Requires Slack to already be connected via the dashboard OAuth flow.","parameters":[{"$ref":"#/components/parameters/Slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["channel_id","channel_name","notify_mode"],"properties":{"channel_id":{"type":"string"},"channel_name":{"type":"string"},"notify_mode":{"type":"string","enum":["all","threshold"]},"score_threshold":{"type":"number","minimum":0,"maximum":100}}}}}},"responses":{"200":{"description":"Slack config updated."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/api-key/rotate":{"post":{"tags":["API key"],"operationId":"rotateApiKey","summary":"Rotate the company API key.","description":"Destructive. Invalidates the current API key immediately. The\nresponse contains the new raw key EXACTLY ONCE — store it now.\n","parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"New raw API key (returned once).","content":{"application/json":{"schema":{"type":"object","properties":{"api_key":{"type":"string","examples":["evidal_sk_REDACTED"]},"key_prefix":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/dns-status":{"get":{"tags":["DNS"],"operationId":"checkDns","summary":"Read DNS verification status for the company subdomain.","parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"DNS status.","content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","nullable":true},"dns_verified":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/billing/status":{"get":{"tags":["Billing"],"operationId":"getBillingStatus","summary":"Per-role billing state.","description":"Active, non-archived roles only.","parameters":[{"$ref":"#/components/parameters/Slug"}],"responses":{"200":{"description":"Per-role billing state.","content":{"application/json":{"schema":{"type":"object","properties":{"roles":{"type":"array","items":{"type":"object","properties":{"role_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"title":{"type":"string"},"role_status":{"type":"string"},"eval_count":{"type":"integer"},"trial_remaining":{"type":"integer"},"needs_payment":{"type":"boolean"}}}},"lead_unlocks_count":{"type":"integer","description":"Total number of $100 candidate lead unlocks ever paid for this company. Not paginated."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/unlocks":{"get":{"tags":["Billing"],"operationId":"listUnlocks","summary":"Paginated list of $100 candidate lead unlocks.","description":"Admin+ only for session callers (financial data is a strictly higher\nsensitivity class than the hiring-team roster). API-key callers have\nimplicit owner scope. Every call writes a `payment.lead_unlock.list`\naudit row.\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Paginated lead unlocks.","content":{"application/json":{"schema":{"type":"object","properties":{"unlocks":{"type":"array","items":{"type":"object","properties":{"candidate_agent_id":{"type":"string","format":"uuid"},"candidate_agent_id_short":{"type":"string","description":"First 8 characters of candidate_agent_id (convenience for logging)."},"amount_cents":{"type":"integer"},"unlocked_at":{"type":"string","format":"date-time"},"stripe_payment_intent_id":{"type":"string","description":"Opaque Stripe identifier. Non-secret — cannot be acted on without the platform secret key."}}}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Session caller lacks admin role (viewers are rejected).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/payment/stripe/checkout":{"post":{"tags":["Billing"],"operationId":"startCheckout","summary":"Start a Stripe Checkout session for a role plan.","description":"Returns `{ checkout_url, session_id }`. The `checkout_url` must be\nopened in a browser — it cannot be completed programmatically.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["slug","plan"],"properties":{"slug":{"type":"string"},"plan":{"type":"string","enum":["full_access","pay_per_eval","lead_unlock"]},"role_id":{"type":"string","format":"uuid"},"candidate_agent_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Stripe Checkout session.","content":{"application/json":{"schema":{"type":"object","properties":{"checkout_url":{"type":"string","format":"uri"},"session_id":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/members":{"get":{"tags":["Members"],"operationId":"listMembers","summary":"List hiring-team members and pending invites (paginated).","description":"Returns an array of current accepted members (`status: active`) and\noutstanding invites (`status: pending`). The `email` field is the\nteam-member email and should be treated as confidential — this is NOT\ncandidate PII, but it is internal company data.\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Paginated members list.","content":{"application/json":{"schema":{"type":"object","properties":{"members":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":["string","null"],"format":"uuid"},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","admin","viewer"]},"status":{"type":"string","enum":["active","pending"]},"invited_at":{"type":"string","format":"date-time"},"accepted_at":{"type":["string","null"],"format":"date-time"}}}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/members/invite":{"post":{"tags":["Members"],"operationId":"inviteMember","summary":"Invite a teammate by email.","description":"Sends an invite email with a secure token. Idempotent: if the email\nalready has a pending invite or is an active member, returns `200`\nwithout resending. API-key callers have implicit owner scope and may\ninvite at any role.\n","parameters":[{"$ref":"#/components/parameters/Slug"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","role"],"properties":{"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","admin","viewer"]}}}}}},"responses":{"200":{"description":"Invite created (or idempotent no-op).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/company/{slug}/members/{userId}":{"delete":{"tags":["Members"],"operationId":"removeMember","summary":"Remove a teammate.","description":"Destructive. Last-owner removal is blocked (returns 400).\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Member removed.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/{slug}/members/{userId}/role":{"post":{"tags":["Members"],"operationId":"updateMemberRole","summary":"Change a teammate's role.","description":"Last-owner demotion is blocked (returns 400).\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role"],"properties":{"role":{"type":"string","enum":["owner","admin","viewer"]}}}}}},"responses":{"200":{"description":"Role updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/{slug}/invites/{inviteId}":{"delete":{"tags":["Members"],"operationId":"revokeInvite","summary":"Revoke a pending invite.","description":"`inviteId` is the `company_members` row UUID (not the secret token).\nReturns 400 if the invite has already been accepted.\n","parameters":[{"$ref":"#/components/parameters/Slug"},{"name":"inviteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Invite revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/company/onboarding/start":{"post":{"tags":["Onboarding"],"operationId":"startOnboarding","summary":"Start a company onboarding session.","description":"Unauthenticated. Creates a pending onboarding session and emails a\nmagic-link verification URL to `owner_email`. The caller polls\n`GET /api/v1/company/onboarding/{sessionId}` until `status` flips to\n`verified`, then POSTs `?action=complete` to create the company.\n\nRate limits: **5 requests/hour per IP**, **100 requests/hour global**\ncircuit breaker. Exceeding either returns `429` (IP) or `503` (global)\nwith a `retry_after_seconds` hint.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner_email","company_name"],"properties":{"owner_email":{"type":"string","format":"email","example":"founder@example.evidal.ai","description":"Email address that will receive the magic-link. Becomes the first owner of the new company."},"company_name":{"type":"string","maxLength":200,"example":"Example Co"},"slug":{"type":"string","description":"Desired company slug. If omitted, derived from `company_name`.","example":"example-co"},"domain":{"type":"string","description":"Optional custom domain to attach (DNS verification happens later).","example":"example.com"},"invite_emails":{"type":"array","items":{"type":"string","format":"email"},"description":"Additional hiring-team members to invite when the session is completed.","example":["teammate@example.evidal.ai"]}},"additionalProperties":false}}}},"responses":{"201":{"description":"Session created; verification email dispatched.","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending_email_verification"]},"verification_email_sent_to":{"type":"string","format":"email"},"expires_at":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/company/onboarding/{sessionId}":{"get":{"tags":["Onboarding"],"operationId":"getOnboardingStatus","summary":"Poll the status of an onboarding session.","description":"Unauthenticated. Returns the current session status. Callers typically\npoll this until `status === 'verified'`, then POST `?action=complete`.\n\nRate limit: **60 requests/hour per IP**.\n","security":[],"parameters":[{"$ref":"#/components/parameters/OnboardingSessionId"}],"responses":{"200":{"description":"Session status.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["pending_email_verification","verified","completed","cancelled","expired"]},"company_slug":{"type":"string","nullable":true,"description":"Present only when `status === 'completed'`."}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Onboarding"],"operationId":"completeOnboarding","summary":"Complete a verified onboarding session (create the company).","description":"Unauthenticated. Exchanges a `verified` onboarding session for a new\ncompany record, issues the first `evidal_sk_` API key, and dispatches\nany queued invite emails.\n\nThe POST method on this path is routed by the required `action` query\nparam: `?action=complete` finalises the session (this operation);\n`?action=cancel` (see `cancelOnboarding`) terminates it.\n","security":[],"parameters":[{"$ref":"#/components/parameters/OnboardingSessionId"},{"name":"action","in":"query","required":true,"description":"Must be `complete` for this operation.","schema":{"type":"string","enum":["complete"]}}],"responses":{"200":{"description":"Company created.","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string"},"api_key":{"type":"string","description":"The first `evidal_sk_` API key. Shown exactly once — store it immediately."},"admin_url":{"type":"string","format":"uri"},"subdomain_url":{"type":"string","format":"uri"},"docs_url":{"type":"string","format":"uri"},"mcp_endpoint":{"type":"string","format":"uri"},"cname_target":{"type":"string"},"invites":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string","format":"email"},"status":{"type":"string"},"error_message":{"type":"string","nullable":true}}}},"next_steps":{"type":"array","items":{"type":"string"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Slug already taken or session already completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/company/onboarding/{sessionId}#cancel":{"post":{"tags":["Onboarding"],"operationId":"cancelOnboarding","summary":"Cancel an onboarding session.","description":"Unauthenticated. Terminates the session. Idempotent — re-calling on\nan already-terminal session returns the current status without error.\n\nNOTE: the actual request URL is `/api/v1/company/onboarding/{sessionId}`\n(no `#cancel` fragment — that is only a documentation anchor to\ndisambiguate this operation from `completeOnboarding`). The method is\n`POST` and the `action=cancel` query param is required.\n","security":[],"parameters":[{"$ref":"#/components/parameters/OnboardingSessionId"},{"name":"action","in":"query","required":true,"description":"Must be `cancel` for this operation.","schema":{"type":"string","enum":["cancel"]}}],"responses":{"200":{"description":"Session cancelled (or was already in a terminal state).","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["cancelled","completed","expired"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"410":{"description":"Session already expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/candidate/erase":{"post":{"tags":["Candidate self-service"],"operationId":"eraseCandidateData","summary":"Permanently erase all of a candidate's data (GDPR right to erasure).","description":"Hard-deletes everything Evidal holds about a candidate — applications,\nevaluation sessions and transcripts, scores, unlock records, and the\ncandidate account itself, across every company they applied to. The\ndeletion is immediate and irreversible.\n\n**Authentication (no Bearer API key):**\n- **Candidate self-service** — present a verified candidate session\n  (`evi_cand_session` HttpOnly cookie, or `Authorization: Bearer\n  <candidate-session JWT>`). The target candidate is taken from the\n  verified token, never from the body; a body `candidate_id` that\n  does not match the token returns `403`.\n- **Evidal staff (DSAR fulfilment)** — a staff Supabase session may\n  target any candidate by `candidate_id` or `contact_email`.\n\nCompany API keys are deliberately rejected: erasure is global across\ncompanies, so only the candidate themself or Evidal ops may trigger\nit.\n\n**Idempotent** — erasing an already-erased (or unknown) candidate\nreturns `200` with `already_erased: true` rather than `404`, to avoid\nan enumeration oracle and keep DSAR tooling retry-safe.\n\nRate limit: **10 requests/minute per IP**.\n","security":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"candidate_id":{"type":"string","format":"uuid","description":"Target candidate (staff callers only; for candidate-session callers it must match the token's candidate)."},"contact_email":{"type":"string","format":"email","description":"Alternative staff-only target selector (DSAR-by-email)."}},"additionalProperties":false}}}},"responses":{"200":{"description":"Erasure completed (or there was nothing left to erase).","content":{"application/json":{"schema":{"type":"object","properties":{"erased":{"type":"boolean"},"already_erased":{"type":"boolean","description":"True when no data remained for the target."},"deleted_counts":{"type":"object","description":"Rows deleted per table.","additionalProperties":{"type":"integer"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Candidate-session caller named a different candidate_id than their own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Partial failure — some tables could not be deleted. Safe to retry (idempotent).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"BearerApiKey":{"type":"http","scheme":"bearer","bearerFormat":"evidal_sk_...","description":"Company-scoped API key generated at\n`/admin/{company}/integrations`. Pass as\n`Authorization: Bearer evidal_sk_...`.\n"}},"parameters":{"Slug":{"name":"slug","in":"path","required":true,"description":"Company slug.","schema":{"type":"string"}},"RoleId":{"name":"roleId","in":"path","required":true,"description":"Role UUID or role slug.","schema":{"type":"string"}},"CandidateAgentId":{"name":"candidateAgentId","in":"path","required":true,"description":"Candidate-agent UUID.","schema":{"type":"string","format":"uuid"}},"OnboardingSessionId":{"name":"sessionId","in":"path","required":true,"description":"Onboarding session UUID returned by `startOnboarding`.","schema":{"type":"string","format":"uuid"}}},"responses":{"BadRequest":{"description":"Invalid request parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing, invalid, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"Unexpected server error. Body includes an `errorId` for correlation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded. `retry_after_seconds` indicates when to try again.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"retry_after_seconds":{"type":"integer"}}}}}}},"schemas":{"Company":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"domain":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"logo_url":{"type":"string","format":"uri","nullable":true},"dns_verified":{"type":"boolean"},"notification_email":{"oneOf":[{"type":"null"},{"type":"array","items":{"type":"string","format":"email"}}]}}},"Role":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"title":{"type":"string"},"level":{"type":"string"},"active":{"type":"boolean"},"role_status":{"type":"string"},"ai_native_required":{"type":"boolean"},"eval_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}},"CandidateSummary":{"type":"object","description":"Candidate record as returned by `list_candidates` (list-shape). NEVER\nincludes `display_name` or `contact_email` — the list endpoint is PII-\nfree regardless of the candidate's lead-unlock state (bulk-dump\nprevention). Use `get_candidate` to retrieve PII for a single\nunlocked candidate.\n","properties":{"id":{"type":"string","format":"uuid"},"role_id":{"type":"string","format":"uuid"},"role_slug":{"type":"string","nullable":true},"role_title":{"type":"string","nullable":true},"submission_mode":{"type":"string"},"completeness_score":{"type":"integer","nullable":true},"decision":{"type":"string","nullable":true,"enum":["advance","reject","hold",null]},"decision_at":{"type":"string","format":"date-time","nullable":true},"submitted_at":{"type":"string","format":"date-time","nullable":true},"eval_status":{"type":"string","nullable":true},"score":{"type":"object","nullable":true,"properties":{"total":{"type":"integer","nullable":true},"flagged":{"type":"boolean","nullable":true},"scored_at":{"type":"string","format":"date-time","nullable":true}}},"lead_unlocked":{"type":"boolean","description":"True when the $100 candidate lead unlock has been paid for this candidate_agent_id + company."}}},"Candidate":{"type":"object","description":"Candidate record as returned by `get_candidate` (single-candidate\nshape). Includes `display_name` and `contact_email` ONLY when\n`lead_unlocked=true` for this candidate. When not unlocked, those\nkeys are ABSENT from the response (not null). Every PII-returning\nresponse writes one `candidate.pii_read` audit row with non-PII\nmetadata (candidate_agent_id, lead_unlock_id, auth_type,\napi_key_prefix).\n","properties":{"id":{"type":"string","format":"uuid"},"role_id":{"type":"string","format":"uuid"},"role_slug":{"type":"string","nullable":true},"role_title":{"type":"string","nullable":true},"submission_mode":{"type":"string"},"completeness_score":{"type":"integer","nullable":true},"decision":{"type":"string","nullable":true,"enum":["advance","reject","hold",null]},"decision_at":{"type":"string","format":"date-time","nullable":true},"submitted_at":{"type":"string","format":"date-time","nullable":true},"eval_status":{"type":"string","nullable":true},"score":{"type":"object","nullable":true,"properties":{"total":{"type":"integer","nullable":true},"flagged":{"type":"boolean","nullable":true},"scored_at":{"type":"string","format":"date-time","nullable":true},"signal_scores":{"type":"object","nullable":true,"description":"EVI-262 signal trace — one key per signal id with {score:0|0.5|1|null, evidence:string}.","additionalProperties":true},"dimension_levels":{"type":"object","nullable":true,"description":"EVI-262 per-dimension levels {numeric_0_to_1, level, final} where final applies any guardrail cap.","additionalProperties":true},"guardrail_overrides":{"type":"object","nullable":true,"description":"EVI-262 map of dimension key to fired guardrail IDs that lowered the level.","additionalProperties":true}}},"lead_unlocked":{"type":"boolean","description":"True when the $100 candidate lead unlock has been paid for this candidate_agent_id + company."},"lead_unlocked_at":{"type":"string","format":"date-time","nullable":true,"description":"ISO timestamp of the lead unlock, or null if `lead_unlocked` is false."},"display_name":{"type":"string","nullable":true,"description":"Candidate display name. Present ONLY when `lead_unlocked=true` for this candidate. Absent otherwise."},"contact_email":{"type":"string","format":"email","nullable":true,"description":"Candidate contact email. Present ONLY when `lead_unlocked=true` for this candidate. Absent otherwise."}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"errorId":{"type":"string","description":"Correlation ID present on 5xx responses."}}}}}}