Post a job from your own system
Updated
If your roles already live somewhere else — an ATS, an internal tool, a spreadsheet somebody guards — this is how they reach Orbit without anyone retyping them.
What you need
A key with the read and manage jobs scope, created in Orbit → Settings → API keys. A read-only key answers 403 on every request on this page, deliberately: publishing spends your credits, and the key that renders a careers site should not also be able to post advertisements.
export ORBIT_API_KEY="…a key with read and manage jobs…"
The shape: two calls
Creating and publishing are separate.
POST /v1/jobscreates a draft. Nothing is public. Nothing is charged.POST /v1/jobs/{jobId}/publishtakes it live and charges the publish fee.
That is deliberate. Publishing puts the role on your hosted job page, lets candidates apply, enters it into the Wipperoz job seeker feed and starts the AI matching run. A single call that did all of that would mean a key in a misconfigured loop spending your balance before anyone read a log.
It also leaves a person in the loop when you want one: create the drafts from your system, and let a recruiter publish them from Orbit after reading them. If you do not want one, call publish yourself. Both are supported; the choice is yours rather than ours.
Create the draft
curl -X POST https://api.wipperoz.com/v1/jobs \
-H "Authorization: Bearer $ORBIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ats-job-4471" \
-d '{
"title": "Senior Backend Engineer",
"description": "<p>Own the services behind our hiring pipeline.</p>",
"responsibilities": ["Design and ship services on AWS Lambda and DynamoDB"],
"skills": [
{"name": "TypeScript", "level": "required"},
{"name": "DynamoDB", "level": "nice_to_have"}
],
"employmentType": "full_time",
"contractType": "permanent",
"salary": {"min": 150000, "max": 180000, "currency": "AUD", "period": "year"},
"location": {"country": "AU", "state": "NSW", "city": "Sydney", "remotePolicy": "hybrid"}
}'
{
"job": {
"id": "job_01J9A0B1C2EXAMPLE",
"status": "draft",
"title": "Senior Backend Engineer",
"applyLink": "https://www.wipperoz.com/en/apply/job_01J9A0B1C2EXAMPLE?src=careers&account=acc_01J8Z3EXAMPLE"
}
}
Keep the id. It is how you publish, edit and close the role later, and it is the id you should store against the role in your own system.
Only title is required. Everything else is optional here — description becomes required at publish time, and nothing else ever blocks it. description takes HTML, which is what the Orbit editor produces; plain text is fine too.
Publish it
curl -X POST https://api.wipperoz.com/v1/jobs/job_01J9A0B1C2EXAMPLE/publish \
-H "Authorization: Bearer $ORBIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ats-job-4471-publish" \
-d '{"expiryDays": 30}'
{
"job": {"id": "job_01J9A0B1C2EXAMPLE", "status": "published", "url": "…"},
"creditsCharged": 500,
"firstPublish": true
}
expiryDaysis30,60,90, ornullfor no expiry.nullis the default: a role that disappears on a date nobody chose is worse than one that stays up.creditsChargedis what this call cost. Re-publishing a role that has been live before — a closed role you are reopening — keeps its URL and charges nothing, andfirstPublishtells you which happened.- Not enough credits answers
402with the balance, and nothing is written. Top up and call the same endpoint again. - A role missing its title or description answers
422with amissinglist rather than a vague400.
Edit a role
curl -X PATCH https://api.wipperoz.com/v1/jobs/job_01J9A0B1C2EXAMPLE \
-H "Authorization: Bearer $ORBIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"salary": {"min": 170000, "max": 200000, "currency": "AUD", "period": "year"}}'
Send only the fields you are changing; the rest are left alone.
On a draft the change applies immediately. On a published role the new words are staged rather than replacing what candidates are currently reading, and the response says so:
{"job": {"…": "…as it still reads today"}, "pendingRevision": true}
People are applying against the words on the page, and changing them underneath an open application changes what someone agreed to after they agreed to it. So a recruiter promotes the edit in Orbit — or you promote it yourself in the same call:
-d '{"salary": {...}, "publishEdits": true}'
Two fields skip the queue and always apply at once: expiresAt and maxMatches. Both exist to stop something, and holding “stop at fifty matches” behind a promotion would keep spending the credits the cap was set to save.
Close a role
curl -X POST https://api.wipperoz.com/v1/jobs/job_01J9A0B1C2EXAMPLE/close \
-H "Authorization: Bearer $ORBIT_API_KEY"
The hosted page stops accepting applications and says the role has closed, the role leaves the job seeker feed, and the CV access the role granted your recruiters ends with it — the reason for that access was an open application.
Applications you already received stay, and stay readable in Orbit. Closing is not deleting, and this API has no delete. Only a published role can be closed; a draft or an already-closed role answers 409.
Retries that do not cost you twice
Send an Idempotency-Key on every POST. Any value that identifies the intent — the role’s id in your own system is ideal.
- A retry with the same key returns the first answer, with
Idempotent-Replay: true. No second advertisement, no second charge. - The same key with a different body answers
409. Two intents wearing one name is a bug worth being told about. - Keys are remembered for 24 hours, per account.
- A request that was refused releases its key, so a
402you fix by topping up can be retried with the same key.
Without the header, two identical creates make two advertisements. Networks time out; send the header.
Keeping a sync honest
- Store our
idagainst your role. It is the only stable handle. - Close what closed upstream. A stale advertisement costs a candidate their afternoon, which is worse than an absent one.
- Do not re-create on every sync.
PATCHthe role you already created; a freshPOSTmakes a second advertisement with a second fee and a second URL. - Rate limit: 120 requests a minute per key. Forty roles is forty calls, comfortably inside it.
Reference
Create a draft jobPOST /v1/jobs
Publish a jobPOST /v1/jobs/{jobId}/publish
Edit a jobPATCH /v1/jobs/{jobId}
Close a jobPOST /v1/jobs/{jobId}/close