# evidal:agent:v2 — Data Platform Engineer @ Bloodcuff Evidal is an AI-native hiring platform. Instead of resumes, candidates apply by having their AI agent talk to Evidal's onboarding bot. The bot extracts a structured experience profile through conversation, then runs an automated evaluation. This file tells your agent exactly how to apply. There is no document upload — a resume or notes the candidate has already shared are a fine starting index, but ask the candidate directly for the specifics that documents don't carry. > **First time applying via a bot?** Read the candidate agent briefing first: https://evidal.bloodcuff.com/api/v1/candidate-briefing.md > The briefing covers what to actually *say*. This file covers how to *call* the API. ## Role: Data Platform Engineer (Senior) Company: Bloodcuff Role ID: c7c9fa9d-3b4f-4fd2-b4ae-510f6bdbdc7a AI-native required: No allowed_modes: [api, poll, web, webhook] ### What this role needs **Required experience — be ready to discuss a real project for each; one project can cover several** - Python: Stated as the primary implementation language for pipeline and data engineering work. how the team reads this: Building and maintaining ingestion, transformation, and serving pipeline logic day-to-day. - SQL and warehouse modeling: Warehouse modeling for product and regulatory analytics is an explicit responsibility. how the team reads this: Writing dbt models, authoring transformations in Snowflake or equivalent, and structuring analytical schemas. - Streaming and batch pipeline engineering: Ingestion from device fleets via streaming (Kinesis) and nightly batch jobs are core responsibilities. how the team reads this: Designing, operating, and debugging Kinesis-based streaming pipelines and scheduled batch workflows. - AWS cloud data platform: The full stack runs on AWS; services like Kinesis, S3, and surrounding infra are the operating environment. any of: Kinesis, S3, Glue, Lambda, EMR how the team reads this: Configuring and operating S3 storage, Kinesis streams, IAM, and adjacent AWS primitives for data workloads. - Data quality contracts: Owning data quality contracts is an explicit stated responsibility critical to clinical-grade data. how the team reads this: Defining, enforcing, and monitoring schema and value-level contracts across pipeline stages. **Strongly preferred experience — an example you can talk through is enough** - dbt or comparable transformation framework: dbt is called out as the transformation layer for warehouse modeling. any of: dbt, SQLMesh, Dataform how the team reads this: Authoring and maintaining dbt models, tests, and documentation for the analytical warehouse. - Data lineage for regulated environments: Partnering with compliance teams on data lineage for audits is explicit; clinical-grade context raises the stakes. how the team reads this: Documenting and surfacing lineage metadata to satisfy regulatory audit requirements in coordination with compliance and firmware teams. ### Must-haves - 5+ years building production data pipelines in Python and SQL - Streaming ingestion at scale (Kinesis, Kafka or similar) - Warehouse modeling with dbt or similar on Snowflake/BigQuery/Redshift - Owned reliability of scheduled + streaming jobs (SLOs, on-call) - Data quality contracts and lineage for audited environments ### Nice-to-haves - Healthcare or regulated-data experience - Cost optimisation of warehouse workloads - Terraform/IaC for data infrastructure --- ## Schema Requirements (v2) The current canonical schema is **v2** — an 8-section rubric that gives the eval bot a structured target to probe-and-verify rather than rediscover. Submit `meta.schema_version: "2.0"` and a populated `rubric` object via `POST /api/v1/session/{id}/schema`. The legacy v1 shape is still accepted (see `## Legacy v1` below). ### The 8 rubric sections 1. **`rubric.about_work`** *(required)* — current_role, domains (1–2), work_types (1–2), time_split (activity + pct entries summing 95–105), recency, brief_context. `recency` MUST be one of `within_12mo | one_to_three_y | three_plus_y`. 2. **`rubric.projects`** *(required, 1–2 entries)* — ALL of these per entry are required or you get a **422**: `context`, `diagnosis` (object with `reasoning` required; `wrong_assumption` optional), `actions`, `decision_tradeoffs`, `role_on_project` (your specific role on THIS project — distinct from about_work.current_role), `first_signal` (what first signalled the problem was worth solving), `other_explanations_considered` (other causes/solutions you weighed before choosing). 3. **`rubric.tools`** *(required, 1+ entries)* — tool, used_for, step_in_workflow, why_over_alternatives, personal_contribution. 4. **`rubric.problem_solving`** *(required)* — what_check_first, what_determines_next_step, how_choose_between_causes. 5. **`rubric.key_decision`** *(required)* — options (string array), chosen, why, tradeoff_accepted. 6. **`rubric.role_ownership`** *(required)* — what_you_owned, decisions_you_made (string array), what_others_owned. 7. **`rubric.role_transfer`** *(required, 1+ entries — one per JD must-have)* — tool, direct_experience (boolean), application_or_transfer. 8. **`rubric.links`** *(optional)* — github, portfolio, other (all optional URL strings). NEVER put a GitHub token here; use the dedicated `signals.github_token` field on `POST /api/v1/session/{id}/github` (token is held in memory for eval and is never persisted). If the candidate has private repos the token won't show, add one sentence summarizing that work in `signals.private_repos_note` (optional, free text, self-reported — see the `?version=v2` schema template below). If you submit `meta.schema_version: "1.0"` AND a populated `rubric` block, the request is rejected with `400 mixed-version` — pick a single version per request. ### Complete required-field checklist (omit any and you get a 422) The v2 validator (`validateExperienceSchemaV2`) requires ALL of the following. This is the authoritative list — match it exactly. - `candidate`: `name`, `contact`, `consent` (boolean) - `experience`: must be an **array** (it MAY be empty in v2 — `rubric.projects` is the source of truth; if you do include entries each must be a full v1-shape entry) - `signals`: `ai_tooling` (string array), `ai_native_examples` (string array) - `rubric.about_work`: `current_role`, `domains` (1–2), `work_types` (1–2), `time_split` (each entry `{ activity, pct }`; pct sum in **[95, 105]**), `recency` (`within_12mo | one_to_three_y | three_plus_y`), `brief_context` - `rubric.projects` (1–2 entries), each entry: `context`, `diagnosis.reasoning`, `actions`, `decision_tradeoffs`, `role_on_project`, `first_signal`, `other_explanations_considered` *(`diagnosis.wrong_assumption` is OPTIONAL)* - `rubric.tools` (1+ entries), each entry: `tool`, `used_for`, `step_in_workflow`, `why_over_alternatives`, `personal_contribution` - `rubric.problem_solving`: `what_check_first`, `what_determines_next_step`, `how_choose_between_causes` - `rubric.key_decision`: `options` (non-empty string array), `chosen`, `why`, `tradeoff_accepted` - `rubric.role_ownership`: `what_you_owned`, `decisions_you_made` (non-empty string array), `what_others_owned` - `rubric.role_transfer` (1+ entries), each entry: `tool`, `direct_experience` (boolean), `application_or_transfer` - `meta`: `submission_mode` (`api | poll | web`), `schema_version` (`"2.0"`), `submitted_at`, `completeness_score` (number) `rubric.links` and `rubric.ai_assistance` are OPTIONAL. A 422 response lists every failing field path (e.g. `rubric.projects[0].first_signal is required`) — read the `errors` array and resubmit. --- ## Quick Start This is the recommended way to apply: a real back-and-forth conversation with Evidal's onboarding bot, so it can ask follow-up questions and draw out specifics a one-shot submission cannot. ```bash # 1. Start a session for this role curl -X POST https://evidal.bloodcuff.com/api/v1/session/start \ -H "Content-Type: application/json" \ -d '{"mode": "poll", "role_slug": "data-platform-engineer"}' # Returns: { "id": "", "status": "active", "role": { "title": "Data Platform Engineer" } } # 2. Get the opening message curl https://evidal.bloodcuff.com/api/v1/session//next # 3. Respond to the bot curl -X POST https://evidal.bloodcuff.com/api/v1/session//respond \ -H "Content-Type: application/json" \ -d '{"message": "Hi, I am [Name]. I have been working on..."}' # 4. Repeat steps 2–3 until schema_complete: true (v2.2 sessions: also packet_status: "pending") # 5. v2.2 sessions (poll / web): you have ALREADY given Evidal your answers in the # conversation above — there is NO separate packet to fetch or poll. Submit # DIRECTLY with POST /packet/confirm. (Do NOT poll GET /packet here: for a # conversational session the evidence packet is built in the BACKGROUND after # you confirm, so GET /packet 404s until then — polling it loops forever.) # Legacy v1 sessions: review the preview — revise up to 2 times or finalize. # 5a. BEFORE either call: show your candidate the bot's recap VERBATIM, ask if # they'd like to add another project or expand anything, then have THEM type # the word "confirmed". Send it as {"human_confirmation": "confirmed"} — # without it, confirm/finalize returns 422 and submits nothing. # 6. confirm/finalize returns { status: "submitted" } and is TERMINAL: no score, no # eval result is ever returned to you — the evaluation is internal (hiring team only). ``` --- ## Advanced: raw-API submission (testing / integration) > **This is the advanced path, not the recommended one.** Submitting here bypasses the guided onboarding conversation — your agent sends a finished profile in one request, with no back-and-forth, so Evidal cannot ask follow-up questions or draw out specifics the way the conversation does. Use the conversational path above unless you are doing automated testing or an advanced integration. ```bash # 1. (v2 raw API) Start a session in API mode for direct schema POST curl -X POST https://evidal.bloodcuff.com/api/v1/session/start \ -H "Content-Type: application/json" \ -d '{"mode": "api", "role_slug": "data-platform-engineer"}' # Returns: { "id": "", "status": "active", ... } # 2. POST a v2 schema body in one shot curl -X POST https://evidal.bloodcuff.com/api/v1/session//schema \ -H "Content-Type: application/json" \ -d '{ "candidate": { "name": "Alex Chen", "contact": "alex@example.com", "consent": true }, "experience": [], "signals": { "ai_tooling": ["Claude"], "ai_native_examples": ["arch reviews"] }, "rubric": { "about_work": { "current_role": "Senior Backend Engineer", "domains": ["payments"], "work_types": ["ic"], "time_split": [{ "activity": "code", "pct": 100 }], "recency": "within_12mo", "brief_context": "Last 12 months on reconciliation pipeline" }, "projects": [{ "context": "Reconciliation latency 14h", "diagnosis": { "reasoning": "Cron coupling between stages caused tail amplification" }, "actions": "Migrated to event-driven Kafka pipeline", "decision_tradeoffs": "Chose Kafka over SQS for ordered partitions", "role_on_project": "Tech lead — owned the architecture and drove the migration", "first_signal": "On-call paged when the 02:00 settlement batch missed its SLA three nights running", "other_explanations_considered": "We first suspected DB contention and a slow downstream API before isolating the cron coupling" }], "tools": [{ "tool": "Claude", "used_for": "architecture reviews", "step_in_workflow": "before RFC sign-off", "why_over_alternatives": "long context + structured outputs", "personal_contribution": "I author the RFC draft" }], "problem_solving": { "what_check_first": "metric vs control", "what_determines_next_step": "isolation level of the regression", "how_choose_between_causes": "rank by reversibility + cost" }, "key_decision": { "options": ["Kafka", "SQS"], "chosen": "Kafka", "why": "ordered partitions", "tradeoff_accepted": "operational complexity" }, "role_ownership": { "what_you_owned": "Schema + Kafka consumer architecture", "decisions_you_made": ["partition key", "rollback strategy"], "what_others_owned": "Frontend dashboard" }, "role_transfer": [{ "tool": "Go", "direct_experience": true, "application_or_transfer": "5+ years production Go" }] }, "meta": { "submission_mode": "api", "schema_version": "2.0", "submitted_at": "2026-01-01T00:00:00Z", "completeness_score": 90 } }' # Returns HTTP 202: { "status": "packet_review", "next_step": { "kind": "packet_review", "method": "GET", "url": "/api/v1/session//packet", "retry_method": "POST", "retry_url": "/api/v1/session//packet/generate" } } ``` **The schema POST is NOT terminal and returns NO score.** It returns **HTTP 202 Accepted** (not 200) with a `next_step` object pointing at the packet-review step. Step 9 — packet review — runs after schema submission: Evidal generates a `candidate_facing_summary` (the candidate's own evidence, restated — **NOT a score, grade, or evaluation result**) and your agent reviews / optionally corrects / confirms it. Confirming the packet ends your interaction. Check `response.ok` (truthy for both 200 and 202), not `response.status === 200`. The wire body submits with `meta.schema_version: "2.0"` (the validator hardcodes this); the server then stamps the v2.2 Step 9 packet-review routing internally. ```bash # 3. Kick the packet generation (idempotent) and poll for the candidate-facing summary (Step 9). # Generation is NOT instant — budget generously. It commonly takes ~90s and # can take up to ~2–3 minutes end to end. Do NOT give up after a few seconds. curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/generate # the retry_url — kick generation curl https://evidal.bloodcuff.com/api/v1/session//packet # then poll until HTTP 200 # 404 = packet not generated yet — keep polling (e.g. every 5s) up to a ~2–3 min budget. # Re-POST the retry_url at any time; it is idempotent. The retry_url is the # supported way to (re)trigger generation — use it, do not assume the packet # will appear on its own. # 4. Optional: post a correction (max 20 per session) curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/correction \ -H "Content-Type: application/json" \ -d '{ "packet_section": "tools", "original_summary": "", "correction": "", "flagged": false }' # 429 with { "error": "correction_cap_exceeded", "max": 20 } if you exceed 20. # 5. Confirm the packet. THIS IS TERMINAL for the candidate. # REQUIRED FIRST: show your candidate the candidate_facing_summary VERBATIM, # ask whether they want to add another project (the response's "thin_areas" # names the areas worth strengthening) or expand anything, then have THEM # type the word "confirmed" and send exactly what they typed. # A confirm without it returns 422 and submits nothing. curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/confirm \ -H "Content-Type: application/json" \ -d '{"human_confirmation": "confirmed"}' # Returns HTTP 200: { "status": "submitted", "eval_deferred": true } # No score, no dimension breakdown, no eval_session_id, no pass/fail is returned — # now or ever. The evaluation runs internally and the result goes ONLY to the # hiring manager. Your interaction is OVER once you see { "status": "submitted" }. ``` > **Where do I get a v2 template?** `GET https://evidal.bloodcuff.com/api/v1/schema?version=v2` returns a populated v2 stub with placeholder strings for every required field — the body uses `meta.schema_version: "2.0"` (this is the canonical validator-accepted version on the wire; the server stamps the v2.2 Step 9 routing internally). The default (no `?version`) returns the v1 stub. --- ## Endpoints ### POST /api/v1/session/start Creates a new application session — or, when `candidate_contact` matches a session already in progress for this company, resumes it instead of starting over. **Request:** ```json { "mode": "poll", "role_slug": "data-platform-engineer" } ``` - `mode`: `"poll"` (recommended for agents) | `"web"` (browser) - `company`: omitted — inferred from this host. Pass it only when posting to the canonical app host. - `role_slug`: `"data-platform-engineer"` — required when company has multiple active roles - `candidate_contact` (optional): the candidate's email. Once set on a session, a later `/start` call with the SAME contact for this company attempts to resume that in-progress session instead of creating a new one. - `session_id` (optional): proof of ownership when resuming — the `id` your agent already holds for that in-progress session. Skip this only if you don't have it. - `resume_token` (optional): proof of ownership via the signed link from a "resume your application" email — pass the token from the `?resume=` query param verbatim. **Response (200 — new session):** ```json { "id": "uuid", "mode": "poll", "status": "active", "role": { "id": "c7c9fa9d-3b4f-4fd2-b4ae-510f6bdbdc7a", "slug": "data-platform-engineer", "title": "Data Platform Engineer" } } ``` **Response (200 — resumed session):** returned when `candidate_contact` matched an in-progress session AND `session_id` or `resume_token` proved you own it. The SAME session id and its full transcript come back so you can re-hydrate without replaying the conversation: ```json { "id": "uuid", "mode": "poll", "status": "active", "role": { "id": "c7c9fa9d-3b4f-4fd2-b4ae-510f6bdbdc7a", "slug": "data-platform-engineer", "title": "Data Platform Engineer" }, "resumed": true, "conversation_history": [ /* prior turns */ ] } ``` **Response (200 — resumable, no proof supplied):** returned when `candidate_contact` matched an in-progress session but neither `session_id` nor `resume_token` was supplied to prove ownership. The email address alone is never enough to hand back a session — Evidal instead emails a signed resume link to that address: ```json { "resumable": true, "resume_link_sent": true, "message": "An application is already in progress for this contact. We emailed a link to resume it." } ``` No `id`, `status`, or `conversation_history` is returned in this shape. Retry the same request with the `resume_token` from that email (or with `session_id` if your agent had it stored) to get the resumed-session response above. A matched session that has already passed its 24h TTL (see Rate Limits below) is treated as if none was found — you get a fresh new session instead, never a resume link for a session every other endpoint would already reject. --- ### GET /api/v1/session/{id}/next Returns the bot's current message. Call this after each `/respond` to get the next question. **Response (200):** ```json { "message": "Tell me about a recent project...", "schema_complete": false, "completeness_score": 40 } ``` When `schema_complete: true` and `redirect_to: "preview"` (legacy v1 sessions) — proceed to the preview step below. When `schema_complete: true` and `packet_status: "pending"` (v2.1+ / v2.2 conversational sessions) — the session has captured all of your answers and is ready for Step 9 submission. Stop polling `/next`. **Do NOT poll `GET /packet` for a conversational (poll / web) session** — the evidence packet is built in the BACKGROUND only after you confirm, so `GET /packet` 404s until then and polling it loops forever. Submit directly with `POST /api/v1/session/{id}/packet/confirm` (see "Step 9 — Submitting (poll / web)" below). `GET /packet` polling is only for **Path A** (raw-API `/schema` POST), where the packet is generated pre-confirm. --- ### GET /api/v1/session/{id}/preview Fetch the preview of your captured profile. Review this before finalizing. **Response (200):** ```json { "representation": "Alex Chen — Senior Backend Engineer...", "completeness_score": 84, "signals": { "has_impact_metrics": true, "has_ai_workflow": true, "has_github": true }, "revision_count": 0, "can_revise": true } ``` - `representation`: Plain-text summary of everything captured. Read this carefully. - `signals`: What Evidal detected. If a signal you expect is `false`, consider revising. - `revision_count`: How many revisions you have used (max: 2). - `can_revise`: `false` once revision cap is reached — you must finalize. --- ### POST /api/v1/session/{id}/revise Re-enter the onboarding conversation to strengthen specific sections. Maximum 2 revision passes. **Request:** ```json { "sections": ["experience", "ai_workflow"] } ``` - `sections` (optional): hint which areas to focus on. Accepted values: `"experience"`, `"ai_workflow"`, `"stack"`, `"ownership"`, `"impact"` **Response (200):** ```json { "status": "active", "revision_count": 1, "message": "Let's tighten up your AI workflow section..." } ``` **Response (403 — cap reached):** ```json { "error": "revision_cap_reached", "max": 2, "message": "No more revisions available. Please finalize your application." } ``` After revising, resume the conversation via `/next` and `/respond` as normal, then re-fetch `/preview` when `schema_complete: true` again. --- ### POST /api/v1/session/{id}/finalize Submit your application for evaluation. This is the final step. **Request (required):** ```json { "human_confirmation": "confirmed" } ``` Same requirement as `/packet/confirm`: show your candidate the `/preview` representation verbatim, ask whether they'd like to add another project or expand anything (`POST /revise` puts the session back into the conversation), and have THEM type the word `confirmed`. Send it exactly as typed. **Response (422 — no human confirmation):** the submission is refused and nothing is finalized; the body carries the required field, the required value, and the exact retry shape. An already-finalized session still returns its `already_finalized` success shape with no body. **Response (200):** ```json { "status": "complete", "eval_job_id": "uuid" } ``` **Response (200 — submission recorded, evaluation queued):** ```json { "status": "complete", "eval_job_id": null, "eval_deferred": true, "eval_session_id": null } ``` `eval_deferred: true` means the application is complete and the evaluation is queued on the hiring team's side. Treat it exactly like the first shape: the candidate's interaction is over. Do not retry, re-submit, or poll. **Response (409 — wrong status):** ```json { "error": "Session must be in preview status to finalize" } ``` Once finalized: session is locked, the evaluation begins automatically and runs internally, and the result is delivered ONLY to the hiring team. `eval_job_id` is an opaque submission handle, NOT a score — there is no score, breakdown, or pass/fail returned to the candidate or their bot here or in any later call, and no endpoint to poll for one. No further changes can be made. --- ### POST /api/v1/session/{id}/respond Send the candidate's response. **Request:** `{ "message": "I led the migration of our payment system..." }` **Response (200):** ```json { "message": "What stack were you using?", "schema_complete": false, "completeness_score": 55 } ``` --- ### GET /api/v1/session/{id}/state Check session status at any time. Always returns `200` — this is the one endpoint that stays readable past a session's TTL, so it's the way to detect expiry before hitting a `410` on a mutating call (see Rate Limits). **Response:** `{ "id", "status", "completeness_score", "schema_complete", "messages", "expires_at" }`. `status` reports `"expired"` once the 24h TTL has passed for a session that was still in progress — start a new session rather than continuing to call the other endpoints against this id. --- ## Step 9 — Submitting (v2.2) Step 9 is the terminal step. The mechanics differ by path because of WHEN Evidal has your structured evidence: - **Path A (raw-API `/schema` POST):** you submitted the full rubric inline, so Evidal can build the candidate-facing evidence packet BEFORE you confirm. The flow is: `POST /packet/generate` → poll `GET /packet` until 200 → optional `/packet/correction` → `POST /packet/confirm`. - **Path B (poll) / Path C (web):** you ALREADY gave Evidal your answers turn-by-turn in the conversation. There is NOTHING to pre-generate or poll. Submit DIRECTLY with `POST /packet/confirm`. The evidence packet, rubric extraction, finalize, and eval ALL run in the BACKGROUND after you confirm — you wait for none of it. > **Path B / C bots: do NOT call `POST /packet/generate` and do NOT poll `GET /packet`.** For a conversational session the packet does not exist until AFTER confirm, so `GET /packet` returns 404 indefinitely and `POST /packet/generate` is a no-op (it only rebuilds a packet that already exists). The correct and ONLY submit call for poll / web is `POST /packet/confirm`. Polling the packet endpoints will hang your bot forever. The routes below use knowledge-of-session-id as auth (no candidate JWT). `/packet/confirm` is shared by all paths and is the terminal call; `/packet/generate` + `GET /packet` are the Path-A-only pre-confirm review surface. ### MANDATORY before you confirm — the human confirms, not you **`/packet/confirm` will not accept a submission you decided on by yourself.** It requires `{"human_confirmation": "confirmed"}` — the word your candidate typed, sent back exactly as they typed it. A call without it returns **HTTP 422** with a body that spells out what is missing and the exact shape to retry with. Do these three things, in order, every time: 1. **Show your candidate the summary of what is about to be submitted, VERBATIM.** Path A: the `candidate_facing_summary` from `GET /packet`. Path B / C: the recap the bot showed you in the conversation. Do not paraphrase it, do not shorten it, and do not describe it in your own words — the point is that the person sees what Evidal actually understood, not your rendering of it. 2. **Ask whether they want to add anything.** Ask ONE question: would they like to mention another project — `GET /packet` returns `thin_areas`, the areas of the role their answers say least about, so name those — or add more detail to something already described? If yes, submit the addition first (see "Adding one more project" below), regenerate/re-review, and show them the updated summary before asking again. 3. **Ask them to type the word `confirmed` themselves**, and send back exactly what they typed: ```bash curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/confirm \ -H "Content-Type: application/json" \ -d '{"human_confirmation": "confirmed"}' ``` Surrounding whitespace is ignored and case does not matter. No other word is accepted — not "yes", not "ok", not "confirm". Do not type it on their behalf: this is the one step in the whole flow that is theirs, and the audit trail records that a human confirmed. **Adding one more project.** Use the revision path your session already has — there is no new endpoint: - **Path B / C (conversational):** `POST /api/v1/session/{id}/reopen` puts the session back to `active`, then keep answering with `POST /respond` as normal. The bot re-recaps when the schema completes again. - **Path A (raw API):** `POST /api/v1/session/{id}/packet/correction` with the added material, then `POST /packet/generate` and poll `GET /packet` again for the updated summary. Either way: show the human the regenerated summary before you ask for the word. ### Step 9 — Submitting (poll / web) For a Path B / Path C session, once `/next` or `/respond` reports `schema_complete: true` with `packet_status: "pending"`, show your candidate the bot's recap verbatim, ask them the one-more-project question, then: ```bash curl -X POST https://evidal.bloodcuff.com/api/v1/session//packet/confirm \ -H "Content-Type: application/json" \ -d '{"human_confirmation": "confirmed"}' # Returns HTTP 200: { "status": "submitted", "eval_deferred": true } ``` That is the whole of Step 9 for poll / web. The response is terminal — no score, no breakdown, no eval handle, now or ever. > If you are driving the conversation with `POST /respond` rather than confirming over the API, the bot's own recap turn is the gate: it asks for the single word `confirmed` in the chat, and any other answer gets one re-prompt. Same requirement, same word — just delivered in the conversation instead of in a request body. ### GET /api/v1/session/{id}/packet Returns the candidate-facing summary. **Path A only** — this is the pre-confirm review surface for raw-API submissions. **Poll / web bots must NOT poll this endpoint** (the packet is built only AFTER `/packet/confirm` for conversational sessions, so it 404s until then). **Response (200):** ```json { "candidate_facing_summary": { "structured_evidence": { "project_anchor": "...", "tools": [{ "tool": "Kafka", "evidence_excerpts": ["..."] }], "problem_solving": { "evidence_excerpts": ["..."] }, "decision_tradeoff": { "evidence_excerpts": ["..."] }, "ownership_boundaries": { "evidence_excerpts": ["..."] }, "role_transfer": [{ "requirement": "...", "evidence_excerpts": ["..."] }], "artifacts": { "evidence_excerpts": ["..."] } }, "supporting_excerpts": ["..."] }, "candidate_review_status": "pending" | "edited" | "flagged" | "confirmed", "generated_at": "", "reviewed_at": "" | null, "thin_areas": ["", "..."] } ``` `thin_areas` lists the areas of the role your candidate's answers say least about, by the employer's own name for each. Name them when you ask the one-more-project question above. An empty list is normal and means nothing is standing out — ask the question anyway, without naming anything. **Response (404 — packet not generated yet) [Path A]:** keep polling. POST to `/packet/generate` (idempotent) to kick — or re-kick — generation; the retry_url is the supported way to (re)trigger it. **(Poll / web sessions: a 404 here is EXPECTED and PERMANENT until after confirm — do not loop on it; call `POST /packet/confirm` instead.)** **Polling cadence [Path A]:** generation is NOT instant. It commonly takes ~90 seconds and can take up to ~2–3 minutes. Poll roughly every 5 seconds against a generous ~2–3 minute budget, and re-POST `/packet/generate` if you suspect generation stalled. Do not abandon the session after a handful of seconds. This packet is the candidate's own evidence restated for review — it is NOT a score, grade, or evaluation result. ### POST /api/v1/session/{id}/packet/generate **Path A only.** (Re)build the pre-confirm CER packet when the rubric is already present (Path A submits the rubric inline via `/schema`). Idempotent — safe to call multiple times. Returns 409 `packet_not_supported_for_schema_version` for v1.0 / v2.0 sessions (those use the legacy auto-finalize path). > **For poll / web (Path B / C) this endpoint is a no-op:** the rubric is not extracted until after `/packet/confirm`, so there is nothing to build from — the route returns `{ packet_status: "pending" }` but generates NO packet. Do not call it; call `POST /packet/confirm` to submit. **Request:** No body required. **Response (200):** `{ "packet_status": "pending", "session_id": "" }` ### POST /api/v1/session/{id}/packet/correction Append a candidate correction. The next confirm pass will treat the correction as part of the evaluator-facing context. **Request:** ```json { "packet_section": "project_anchor" | "tools" | "problem_solving" | "decision_tradeoff" | "ownership_boundaries" | "role_transfer" | "artifacts" | "supporting_excerpts", "original_summary": "", "correction": "", "flagged": false } ``` **Response (200):** `{ "candidate_review_status": "edited" | "flagged", "correction_count": }` **Response (400):** `correction_too_long` (correction > 10,000 chars), `original_summary_too_long` (original > 10,000 chars), or other validation error. **Response (429 — correction cap reached):** `{ "error": "correction_cap_exceeded", "max": 20 }`. Limit is 20 corrections per session — well above any plausible legitimate UI session. ### POST /api/v1/session/{id}/packet/confirm Confirm the packet. **This is the terminal API for v2.1+ sessions** (the equivalent of `/finalize` for v1). Once it returns, the candidate's interaction is complete. **Request (required):** ```json { "human_confirmation": "confirmed" } ``` The word your candidate typed, sent exactly as typed, after you showed them the summary verbatim and asked the one-more-project question. See "MANDATORY before you confirm" above. **Response (422 — no human confirmation):** the submission is refused and NOTHING is recorded. The body names the missing field, the required value, the steps to take first, and the exact retry shape: ```json { "error": "human_confirmation_required", "required_field": "human_confirmation", "required_value": "confirmed", "retry": { "method": "POST", "url": "/api/v1/session/{id}/packet/confirm", "body": { "human_confirmation": "confirmed" } } } ``` Re-confirming a session that has ALREADY been submitted still succeeds with no body — a retry can never brick a submitted application. **Response (200):** ```json { "candidate_review_status": "confirmed", "reviewed_at": "", "status": "submitted", "eval_deferred": true, "known_candidate": false } ``` **This response is terminal and carries NO evaluation result.** (`known_candidate` and `candidate_review_status` are bookkeeping only — neither is a score or evaluation outcome.) There is deliberately no score, no dimension breakdown, no `eval_session_id`, no `status: "complete"`, and no pass/fail in this response — now or in any later call. The evaluation runs internally and asynchronously after submission; its result is delivered ONLY to the hiring manager's dashboard. The candidate and the candidate's bot are never given the score or any evaluation feedback, and there is no endpoint to poll for one. Do not wait for, request, or poll for a result after `{ "status": "submitted" }` — the application is done. --- ## Legacy v1 The v1 schema (single-experience-array shape) is still accepted by `POST /api/v1/session/{id}/schema` for back-compat. New integrations should target v2 (above). Everything below this heading describes the v1 surface and the conversational onboarding-bot flow that produces a v1 schema. ### What the Bot Collects (v1) **Required for submission (completeness ≥ 80):** - Name + contact email + consent - At least one experience entry: company, role, duration, impact, tech stack - AI tools used (Copilot, Claude, ChatGPT, Cursor, etc.) - At least one concrete AI-native workflow example **What helps your application:** - GitHub handle (real code is useful context) - LinkedIn URL (used for fact-checking — mismatches are flagged, not penalized) - Specific metrics: "reduced latency from 800ms to 120ms" > "improved performance" - Multiple experience entries with ownership level (IC / lead / manager) --- ## Evaluation After you confirm the packet (`{ "status": "submitted" }`), your agent profile enters an automated 8-step evaluation that runs **entirely internally**. The result is delivered ONLY to the hiring team's dashboard. The candidate and the candidate's bot are NEVER returned a score, dimension breakdown, pass/fail, or any evaluation feedback, and there is no endpoint to poll for one — submission is terminal. You will not be contacted for screening calls. --- ## Tips for Agents - Answer conversationally. The bot extracts info from natural language — don't dump everything at once. - Be specific: "reduced API p99 from 800ms to 120ms" beats "improved performance." - Include AI workflow examples: "I use Claude for architecture reviews and Cursor for all new code." - Don't fabricate. The eval probes for consistency — inflated claims surface during probing. - Mention GitHub handle if you have it. Real code is useful context. --- ## For LLM Agents (Claude Code / GPT / Cursor / etc.) If you are an AI agent representing a candidate, use this structured first-message template. Front-loading all required fields in one message maximizes your `completeness_score` and gets you to evaluation faster. ### Structured first message template (v2) For Path B / Path C (conversational onboarding bot), front-load the 8 rubric sections in your first turn. The extractor maps natural-language answers onto `rubric.*` fields directly. ``` Hi, I'm [Full Name] and my email is [email@example.com]. I consent to this evaluation. I'm applying for [Role Title]. Here's my structured background: **About my work** (rubric.about_work) - Current role: [title] - Domains: [1–2 domains, e.g. payments, infrastructure] - Work types: [1–2 of: ic, lead, architect, manager] - Time split: roughly [X]% [activity], [Y]% [activity] (must sum ~100) - Recency: [within_12mo | one_to_three_y | three_plus_y] - Brief context: [one sentence on what the last year looked like] **A representative project** (rubric.projects — every field below is required) - Context: [the problem and scope] - Diagnosis: [causal reasoning — and one wrong assumption you held early, if any] - Actions: [what you personally did] - Decision tradeoffs: [the alternatives you weighed and what you gave up] - Role on project: [your specific role on THIS project, e.g. tech lead / IC] - First signal: [what first told you the problem was worth solving] - Other explanations considered: [other causes or solutions you weighed before choosing] **Tools that matter** (rubric.tools — 1+) - [tool] for [used_for], at [step in workflow], chosen over [alternative] because [reason]; my contribution: [what you personally did with it] **Problem-solving stance** (rubric.problem_solving) - I check [X] first, decide next step by [Y], and choose between candidate causes by [Z] **A key decision** (rubric.key_decision) - Options: [A, B, C]; chose [B] because [why]; tradeoff accepted: [what was given up] **Ownership boundary** (rubric.role_ownership) - Owned: [list] - Decisions I made: [list] - Owned by others: [list] **Role transfer** (rubric.role_transfer — one entry per JD must-have) - [JD must-have]: [direct experience? yes/no]; [how I'd apply / what transfers] **Links** (rubric.links — optional; URLs only, never tokens) - GitHub: [URL or "n/a"] - Portfolio: [URL or "n/a"] ``` ### Structured first message template (v1 — legacy) ``` Hi, I'm [Full Name] and my email is [email@example.com]. I consent to this evaluation. I'm applying for [Role Title]. Here's my background: **Most relevant experience:** At [Company], I was a [Title] for [Duration]. I [specific action — what you personally did] which resulted in [measurable outcome — numbers preferred]. My primary stack was [list tools/languages]. I owned [specific scope — what was yours alone]. **A key technical decision I made:** [Describe one real decision, the tradeoff you considered, and what you'd do differently.] **AI in my workflow:** I use [tool] for [specific workflow]. Example: [concrete use case with outcome]. **GitHub:** [handle or "not applicable"] **LinkedIn:** [URL or "not applicable"] ``` ### Full application flow for agents (poll / web — Path B / C) This is the flow for a conversational session (`mode: "poll"` or web). Submission is a SINGLE direct confirm — there is NO packet to fetch or poll. ``` 1. POST /session/start → get session_id 2. GET /session/{id}/next → opening message 3. POST /session/{id}/respond → reply; repeat until schema_complete: true + packet_status: "pending" 3a. Show your candidate the bot's recap VERBATIM. Ask ONE question: another project to mention, or anything to expand on? If yes: POST /session/{id}/reopen, keep answering with /respond, and re-review when it completes again. 3b. Ask them to type the word "confirmed" themselves. You send exactly that. 4. POST /session/{id}/packet/confirm {"human_confirmation":"confirmed"} → returns { status: "submitted" }. TERMINAL. — REQUIRED body. Without it: HTTP 422, nothing submitted, retry shape included. — This is the ENTIRE submit step for poll / web. You already gave Evidal your answers in the conversation; the evidence packet + rubric + finalize + eval all run in the BACKGROUND after this call. You wait for none of it. — Do NOT call /packet/generate and do NOT poll GET /packet here — for a conversational session the packet does not exist until AFTER confirm, so GET /packet 404s indefinitely and polling it hangs your bot forever. — No score, no breakdown, no eval_session_id is returned — now or ever. Stop here. ``` ### Full application flow for agents (raw API — Path A) Path A submits the full rubric inline, so Evidal CAN build a review packet before you confirm: ``` 1. POST /session/start {"mode":"api"} → get session_id 2. POST /session/{id}/schema → submit the full v2 rubric (HTTP 202) 3. POST /session/{id}/packet/generate → kick CER generation (idempotent retry_url) 4. GET /session/{id}/packet → poll for candidate_facing_summary (Step 9) — if 404 / packet_status: "pending": keep polling ~every 5s against a generous ~2–3 min budget (generation commonly takes ~90s; it is NOT instant) — re-POST /session/{id}/packet/generate to re-kick if it stalls (idempotent) — the candidate_facing_summary is your OWN evidence restated for review — NOT a score 5. Optionally: POST /session/{id}/packet/correction (max 20 per session) 5a. Show your candidate the candidate_facing_summary VERBATIM. Ask ONE question: another project covering the response's "thin_areas", or anything to expand on? If yes: /packet/correction → /packet/generate → GET /packet again → re-review. 5b. Ask them to type the word "confirmed" themselves. You send exactly that. 6. POST /session/{id}/packet/confirm {"human_confirmation":"confirmed"} → returns { status: "submitted" }. TERMINAL. REQUIRED body. Without it: HTTP 422, nothing submitted, retry shape included. No score, no breakdown, no eval_session_id is returned — now or ever. Stop here. ``` The v2 flow no longer terminates at `schema_complete: true` — Step 9 (submit) is the terminal phase, and confirming (`{ "status": "submitted" }`) ends the candidate's interaction. **No score or evaluation feedback is ever returned to the candidate or their bot** — the evaluation is internal and its result goes only to the hiring manager. The legacy v1 flow with `/preview` + `/finalize` still applies to v1 sessions. The v2.2 / Step 9 routing is what runs for new conversational sessions and Path A v2 schema POSTs alike. ### What the bot will ask about 1. Your professional background (name, email, consent) 2. A specific project — problem, your role, what you personally owned, measurable impact 3. Your tech stack and how you use your tools in practice 4. How AI fits into your daily work (tools + concrete examples) 5. A decision you made and the tradeoff you considered 6. Something you haven't done and how you'd approach it ### Example strong exchange **Bot:** "Hi there — welcome to Evidal. What's your name and best email?" **Agent:** "Hi, I'm Alex Chen, alex@example.com — I consent to this evaluation. I'm a senior backend engineer with 6 years experience, most recently at Stripe where I led the migration of our reconciliation pipeline from a monolithic cron job to an event-driven system using Kafka and Go. The migration reduced daily settlement errors by 73% and cut pipeline latency from 14 hours to 40 minutes. I personally owned the schema design, the Kafka consumer architecture, and coordinating rollback strategy. My stack is primarily Go, Kafka, PostgreSQL, and Redis. For AI tooling: I use Claude for architecture reviews before any RFC goes to team review, and Cursor for all new code. My GitHub is alexc-dev." **Why this works:** Specific numbers (73%, 14h → 40min), clear ownership ("I personally owned"), named stack, concrete AI workflow, GitHub handle — all in one message. --- ## Webhook Mode (Recommended) — EVI-146 Instead of long-polling `/next`, register a webhook URL at session start and Evidal will POST each bot message to your endpoint as soon as it's ready. Signed with HMAC via Svix for integrity. One-shot signing secret returned at session start — store it immediately, it is not retrievable later. ### Start a webhook-mode session ```bash curl -X POST https://evidal.bloodcuff.com/api/v1/session/start \ -H "Content-Type: application/json" \ -d '{ "mode": "webhook", "role_slug": "data-platform-engineer", "callback_url": "https://agent.example.com/evidal/hook" }' ``` **Response (200):** ```json { "id": "", "mode": "webhook", "status": "active", "role": { "id": "...", "title": "Data Platform Engineer" }, "svix_endpoint_id": "ep_...", "signing_secret": "whsec_...", "signing_secret_warning": "Store immediately — not retrievable later. To rotate, start a new session." } ``` ### Events delivered - `bot_message` — the onboarding bot produced a reply. Payload: `{ event, session_id, message, schema_complete, completeness_score, timestamp, idempotency_key }` - `schema_complete` — final event when the bot is done extracting; the session then proceeds to Step 9 packet review and, after confirm, to internal evaluation. Payload: `{ event, session_id, experience_schema, timestamp, idempotency_key }` — the `experience_schema` is PII (name, email, work history). Handle accordingly. This event carries NO score, no `eval_session_id`, and no evaluation result, and no later event ever will — the evaluation is internal and its result goes only to the hiring manager. ### Signature verification Every delivery includes `svix-id`, `svix-timestamp`, and `svix-signature` headers. Verify with the signing secret returned at session start. **Node:** ```js import { Webhook } from 'svix' const wh = new Webhook(process.env.EVIDAL_SIGNING_SECRET) app.post('/evidal/hook', express.raw({type:'application/json'}), (req, res) => { const evt = wh.verify(req.body, req.headers) // evt.event === 'bot_message' | 'schema_complete' res.status(200).end() }) ``` **Python:** ```py from svix.webhooks import Webhook wh = Webhook(os.environ['EVIDAL_SIGNING_SECRET']) evt = wh.verify(request.body, dict(request.headers)) # raises on invalid signature ``` **Go:** ```go import "github.com/svix/svix-webhooks/go" wh, _ := svix.NewWebhook(os.Getenv("EVIDAL_SIGNING_SECRET")) err := wh.Verify(body, r.Header) // non-nil on invalid signature ``` ### Responding in webhook mode In webhook mode, `POST /api/v1/session/{id}/respond` returns `202 Accepted` with `{ application_id, queued: true }` immediately. The bot's reply is delivered via webhook, not inline in the HTTP body. `GET /session/{id}/next` is not used (returns 400). --- ## Polling Mode (Legacy — 90-day deprecation window) The original Path B polling loop remains active during a 90-day deprecation window. New integrations should prefer **Webhook Mode** above. The poll endpoints (`/next`, `/respond`) continue to return inline bot messages for sessions started with `mode: "poll"`. --- ## Rate Limits - 1 session per candidate per role per 72 hours - Sessions expire 24 hours after creation. Once expired, every state-changing call against that session id — `/next`, `/respond`, `/schema`, `/github`, `/finalize`, `/reanswer`, `/reopen`, `/revise`, `/update-schema`, `/packet/confirm`, `/packet/generate`, `/packet/correction` — returns `410 Gone` with `{ "error": "session_expired" }`. `GET /state` stays readable past expiry (it never 410s) so you can detect this and know to call `POST /session/start` again for a fresh session. - Max 10 sessions per IP per hour ## Full platform docs https://evidal.ai