Genace

JOB_NOT_FOUND and NOT_OWNER: Job not found, or not yours

HTTP 404 JOB_NOT_FOUND

Two adjacent errors with different meanings:

{ "error": { "type": "not_found", "code": "JOB_NOT_FOUND",
             "message": "Job abc123 not found" } }
{ "error": { "type": "forbidden", "code": "NOT_OWNER",
             "message": "You do not have access to this job" } }

404 means no job with that id exists. 403 means it exists and belongs to a different account.

Why they are not merged

Returning 404 for someone else’s job would hide its existence, which is the more privacy-preserving choice. The split is deliberate: you authenticated successfully, so “this is not yours” is more useful to you than “this does not exist”, and knowing a job id exists reveals nothing without access to it.

Practically: a 403 means you are using the wrong API key, not the wrong id. Different account, different environment, a key from a teammate.

The race that makes a real job look missing

The one non-obvious cause. If you submit and immediately fetch, you can query before the write lands:

job = submit(...)
status = get(job["job_id"])   # occasionally 404

It is narrow but real under load. Two ways around it:

# Use the wait parameter instead of an immediate bare GET
get(job_id, params={"wait": 30})

# Or retry a 404 once, briefly, before treating it as final

If a 404 persists past a couple of seconds, the id is genuinely wrong.

Check the id you are sending

Job ids come back as job_id in the submit response:

{ "job_id": "abc123", "state": "queued", "credits_held": 25 }

Common mistakes: reading id instead of job_id, keeping an id from a previous run, or URL-encoding something that needs no encoding.

MISSING_ID is different

Omitting the id entirely — /v1/jobs/ with nothing after it — returns 400 MISSING_ID rather than a 404. That usually means a template rendered an empty variable, the same failure shape as MISSING_API_KEY.

JOB_VANISHED on the SSE stream

One more variant, specific to the streaming endpoint. If a job disappears mid-stream, the stream emits:

{ "code": "JOB_VANISHED", "message": "Job no longer exists upstream" }

Unlike a 404 on a bare lookup, this one arrives inside a successful connection. Handle it in the event handler; it will not throw.

Where these facts come from

  • codebase: src/app/api/v1/jobs/[id]/route.ts — lookup and ownership check
  • codebase: src/app/api/v1/jobs/[id]/cancel/route.ts — same checks on cancel
  • codebase: src/ai/jobs/http.ts — JOB_VANISHED on the SSE path