JOB_NOT_FOUND and NOT_OWNER: Job not found, or not yours
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