Skip to main content

Jobs: define once, reference many

The Jobs catalog stores a job's job_description (prose) and its structured requirements config once, so CV DeepMatch and CV DeepSearch runs can reference it instead of re-sending the full content every time.

The model​

A saved job has:

FieldWhat it is
position_idYour own job identifier ([A-Za-z0-9._-], 1–28 chars) — the same position_id the run APIs use. Optional; unique per client among active jobs.
id (aka job_id)The platform-assigned UUID, returned on create. An alternative reference.
job_descriptionThe JD prose (non-empty string).
configThe structured requirements config — strictly validated on every save with the same contract CV DeepMatch enforces at submit (Position config reference). Must not contain a job_description key.

One job serves both services. CV DeepMatch runs directly on job_description + config. CV DeepSearch composes its single position_metadata blob from the same job:

{ ...config, "job_description": "<the JD prose>" }

Create a job, then reference it from both services​

# 1. Define once
curl -X POST https://platform.zenhire.ai/api/v1/jobs \
-H "X-API-Key: $ZENHIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"position_id": "eng-backend-2026",
"job_description": "Senior backend engineer with 5+ years of Node.js…",
"config": {
"name": "Senior Backend Engineer",
"requirements": {
"workExperience": {
"from": 5, "to": 10, "importance": 5, "time_importance": 3,
"relevant_industries": ["SaaS"],
"industries_config": [{ "name": "SaaS", "importance": 5 }]
},
"skills": {
"importance": 5,
"skills_config": {
"hard_skills": [{ "name": "Node.js", "importance": 5 }],
"requirements": { "minimal_qualifications": true, "preferable_qualifications": true }
}
}
}
}
}'
# → 201 { "id": "6f0d5c2e-…", "position_id": "eng-backend-2026", … }

# 2a. Match a CV against it — no job_description / config needed
curl -X POST https://platform.zenhire.ai/api/v1/cvdeepmatch/submit \
-H "X-API-Key: $ZENHIRE_API_KEY" \
-F "cv_file=@candidate.pdf" \
-F "position_id=eng-backend-2026"

# 2b. Search a corpus for it — no position_metadata needed
curl -X POST https://platform.zenhire.ai/api/v1/cvds/search \
-H "X-API-Key: $ZENHIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "corpus_id": "acme-eng-pool", "position_id": "eng-backend-2026" }'

Both runs record the resolved job in their responses' job_id field (null for pure inline runs), so you can trace every run back to the job it executed against.

Referencing rules (both services)​

  • At least one of position_id / job_id is required on POST /api/v1/cvdeepmatch/submit and POST /api/v1/cvds/search.

  • When both are sent they must resolve to the same saved job (400 INVALID_INPUT otherwise); job_id wins for resolution.

  • A reference with no inline content that resolves to no saved job for your client returns 400 JOB_NOT_FOUND:

    {
    "error": {
    "code": "JOB_NOT_FOUND",
    "message": "No saved job with this position_id/job_id for this client. Provide both config and job_description, or create the job first via POST /api/v1/jobs."
    }
    }

    Fix: send the inline content, or create the job first (POST /api/v1/jobs), or check the reference for typos. Archived jobs no longer resolve.

CV DeepMatch: job_description + config are both-or-neither​

Send both to run with inline content (exactly as before), or neither to run with the referenced job's stored content. Sending exactly one returns 400 INVALID_INPUT — there is no partial merge with stored fields. To change one field of a saved job, use PATCH /api/v1/jobs/{jobId}, never the run endpoint.

CV DeepSearch: position_metadata is optional with a reference​

Omit position_metadata and the search composes it from the saved job. Send it inline and the run uses your blob verbatim (the reference, if it resolves, still populates the run's job_id attribution).

Saving from a run: saveJob: true​

Both submit endpoints accept an optional saveJob flag that writes the inline content to the catalog before running:

  • an existing job under the reference is updated; otherwise a new job is created under position_id. A job_id alone can only update — it never creates (400 INVALID_INPUT: "Cannot create a job with a system id; use position_id.").
  • CV DeepMatch saves the inline job_description + config — already contract-validated for the run itself.
  • CV DeepSearch saves the inline position_metadata only when it is genuinely a job: the blob is decomposed — its reserved job_description key (a non-empty string) becomes the JD prose, the remainder becomes the config — and the config is strictly validated with the save-time contract. On any failure the search returns 400 INVALID_INPUT with per-field detail in error.details.fields, nothing is saved and no run is started (all-or-nothing). Inline searches without saveJob remain unvalidated, exactly as before.

Automatic job_updated on CV DeepSearch​

CV DeepSearch caches a search plan per position and rebuilds it when job_updated: true.

For a run whose content comes from a saved job (a reference-only search, or a saveJob that just updated the job), the platform now derives that flag automatically: it is true exactly when the job changed since the last search of that job against the same corpus (or when it is the first such search). The client-sent job_updated is ignored for those runs — you never need to track plan staleness for saved jobs.

Escape hatch: an inline (unsaved) search keeps the client-sent job_updated exactly as before — the platform cannot know whether ad-hoc content differs from the cached plan.

The run's detail response reports the effective flag in is_job_updated.

Updating and archiving​

  • PATCH /api/v1/jobs/{jobId} — partial update; every provided field is re-validated with the strict save-time rules. Past runs are never altered (each run snapshots the content it executed with); the next search against the updated job rebuilds its plan automatically.
  • DELETE /api/v1/jobs/{jobId} — archive (soft delete): the job leaves list/lookup, run submits can no longer reference it, and its position_id becomes free for reuse. 204 on success.
  • position_id is unique per client among active jobs — a duplicate returns 409 POSITION_ID_CONFLICT.

Reference​